[TOC]
# 變量:作用域,環境和閉包
本章首先介紹如何使用變量,然后詳細介紹它們的工作原理(環境,閉包等)。
## 聲明變量
在JavaScript中,您可以`var`在使用該語句前聲明一個變量:
```js
var foo;
foo = 3; // OK, has been declared
bar = 5; // not OK, an undeclared variable
```
您還可以將聲明與賦值結合,以立即初始化變量:
```
var foo = 3;
```
未初始化變量的值為`undefined`:
```
> var x;
> x
undefined
```
## 背景知識:靜態與動態
有兩個角度可以檢查程序的工作原理:
### 靜態地(詞法地)
您可以在源代碼中檢查程序,而不需要運行它。給定下面的代碼,我們可以實現函數`g`被嵌套在函數`f`中的靜態斷言:
```js
function f() {
function g() {
}
}
```
`lexical`與`static`使用是同義的,因為它們都屬于程序的詞典(單詞, 來源)。
### 動態地
你會檢查執行程序時會發生什么(“在運行時”)。給出以下代碼:
```js
function g() {
}
function f() {
g();
}
```
當我們調用`f()`,它又調用了`g()`。在運行時,`g`被`f`調用代表了一種動態關系。
## 背景知識:變量的作用域
對于本章的其余部分,您應該了解以下概念:
**變量的作用域**
變量的作用域是可訪問的位置。例如:
```js
function foo() {
var x;
}
```
這里,`x`的直接作用域是`foo()`函數。
**詞法作用域**
JavaScript中的變量是詞法作用域的,因此程序的靜態結構決定了變量的范圍(它不受例如被調用函數的位置的影響)。
**嵌套作用域**
如果作用域嵌套在變量的直接作用域內,則在所有這些作用域中可訪問該變量:
```js
function foo(arg) {
function bar() {
console.log('arg: '+arg);
}
bar();
}
console.log(foo('hello')); // arg: hello
```
`arg`的直接作用域是`foo()`,但也可以在`bar()`的嵌套作用域內被訪問。對于嵌套,`foo()`是外部作用域和`bar()`是內部作用域。
**被屏蔽**
如果一個作用域中聲明了一個變量,和一個在周圍的作用域內的名稱相同,那么對這個外部變量的訪問會在內部作用域和內嵌套的所有作用域內被阻塞。對內部變量的更改不會影響外部變量,在內部作用域被保留之后該變量還可以被訪問:
```js
var x = "global";
function f() {
var x = "local";
console.log(x); // local
}
f();
console.log(x); // global
```
在函數`f()`內部,全局變量的`x`被全局變量的`x`屏蔽。
## 變量以函數為作用域
大部分主流編程語言都是塊級作用域:變量“活動在內部”最內部的代碼塊。以下是Java的一個例子:
```java
public static void main(String[] args) {
{ // block starts
int foo = 4;
} // block ends
System.out.println(foo); // Error: cannot find symbol
}
```
在上面的代碼中,變量`foo`只能在直接包圍它的塊中訪問。如果我們嘗試在塊結束后訪問它,我們得到一個編譯錯誤。
相比之下,JavaScript的變量是函數作用域的:只有函數引入新的作用域; 當涉及作用域時,塊會被忽略。例如:
```js
function main() {
{ // block starts
var foo = 4;
} // block ends
console.log(foo); // 4
}
```
換句話說,在`main()`中任何位置都可以訪問`foo`,而不僅僅是在塊內。
## 變量聲明被提升
JavaScript 會提升所有變量聲明,將其移動到其直接作用域的的開頭位置。這樣可以清楚地說明如果在聲明變量之前被訪問,會發生什么:
```js
function f() {
console.log(bar); // undefined
var bar = 'abc';
console.log(bar); // abc
}
```
我們可以看到變量`bar`已經存在于`f()`中的第一行,但它還沒有值; 那就是聲明已經被提升了,而不是轉讓。JavaScript執行`f()`的代碼如下:
```js
function f() {
var bar;
console.log(bar); // undefined
bar = 'abc';
console.log(bar); // abc
}
```
如果您再次聲明了已聲明的變量,則不會發生任何變化(變量的值不變):
```js
> var x = 123;
> var x;
> x
123
```
每個函數聲明也會被提升,但是稍有不同。整個函數都將被提升,而不僅僅是指向它(存儲它)的那個變量的創建(參見[提升](第15章函數.md))。
| 最佳做法:不要害怕提升,但是需要注意。|
|:----|
| 一些JavaScript風格指南建議您只在函數的開頭放置變量聲明,以避免被提升。如果你的功能相對較小(應該是這樣),那么你可以稍微放寬這個規則,靠近它們被使用的位置時聲明變量(例如,在`for`循環中)。這更好地封裝了代碼段。顯然,您應該意識到,封裝只是概念性的,因為函數范圍的提升仍然會發生。|
<table>
<th style="text-align:center">
陷阱:給未聲明的變量賦值,它會成為全局變量
</th>
<tr>
<td>
在草率模式下,給未聲明`var`的變量賦值,會創建一個全局變量:
```js
> function sloppyFunc() {x = 123}
> sloppyFunc()
> x
123
```
幸運的是,嚴格模式下會在發生這種情況時拋出異常:
```js
> function strictFunc() { 'use strict'; x = 123 }
> strictFunc()
ReferenceError: x is not defined
```
</td>
</tr>
</table>
## 通過IIFE引入新的作用域
通常會引入一個新的作用域來限制變量的生命周期。 您可能想要做一個這樣的例子是:`if`語句的“接下來”部分,僅當條件成立時才執行,如果它使用了輔助變量,那我們不希望他們“泄漏”到外部的作用域里:
```js
function f() {
if (condition) {
var tmp = ...; //輔助變量
...
}
// tmp 此處依然存在
// => 不是我們想要的
}
```
如果要為then塊引入新的作用域,可以定義一個函數并立即調用它。這是一個解決方法,也就是模擬了塊級作用域:
```js
function f() {
if (condition) {
(function () { // open block
var tmp = ...;
...
}()); // close block
}
}
```
這是JavaScript中的常見模式。Ben Alman建議使用[立即調用函數表達式](http://benalman.com/news/2010/11/immediately-invoked-function-expression/)(**IIFE**,發音為“iffy”)。一般來說,IIFE如下所示:
```js
(function () { // open IIFE
// inside IIFE
}()); // close IIFE
```
以下是有關IIFE的一些注意事項:
**它立即被調用**
功能關閉括號后面的括號立即調用它。這意味著它的身體立即執行。
**它必須是一個表達式**
如果語句以關鍵字`function`開始,則解析器期望它是一個函數聲明(請參閱[表達式和語句](7.JavaScript語法.md))。但是函數聲明不能立即被調用。因此,通過一個帶圓括號開頭的語句,我們告訴解析器,該關鍵字`function`是一個函數表達式的開頭。**因為在圓括號內,只能有表達式**。
**結尾的分號是必需的**
如果您在兩個**IIFE**之間忘記了它,那么您的代碼將不再工作:
```js
(function () {
...
}()) // 沒有分號
(function () {
...
}());
```
上述代碼被解釋為函數調用 - 第一個IIFE(包括括號)是要調用的函數,第二個IIFE是參數。
| 注意 |
| :---: |
| IIFE會增加成本(理解和性能方面),所以在`if`語句中使用它是沒有意義的。前面的例子是出于教學的原因。|
### IIFE變體:前綴操作符
您還可以通過**前綴運算符**強制執行表達式上下文。 例如,你可以通過邏輯的非運算符:
```js
!function () { // open IIFE
// inside IIFE
}(); // close IIFE
```
或通過`void`操作符(請參閱[void操作符](第9章運算符.md)):
```js
void function () { // open IIFE
// inside IIFE
}(); // close IIFE
```
使用前綴運算符的優點是忘記終止分號不會造成麻煩。
### IIFE變體:預內置表達式上下文
請注意,如果您已經在表達式上下文中,則不需要強制執行IIFE的表達式上下文。那么你不需要圓括號或前綴操作符。例如:
```js
var File = function () { // open IIFE
var UNTITLED = 'Untitled';
function File(name) {
this.name = name || UNTITLED;
}
return File;
}(); // close IIFE
```
在前面的例子中,有兩個不同的變量名稱`File`。一方面,這個函數只能在IIFE內部直接訪問。另一方面,在第一行中聲明了變量。它被賦值為在IIFE中返回的值。
### IIFE變體:傳參的IIFE
可以使用參數來定義IIFE內部的變量:
```js
var x = 23;
(function (twice) {
console.log(twice);
}(x * 2));
```
這類似于:
```js
var x = 23;
(function () {
var twice = x * 2;
console.log(twice);
}());
```
### IIF的應用
IIFE使您能夠將私有數據附加到函數上。那么你就不需要聲明一個全局變量可以將函數與其狀態緊密地打包在一起。避免污染全局命名空間:
```js
var setValue = function () {
var prevValue;
return function (value) { // define setValue
if (value !== prevValue) {
console.log('Changed: ' + value);
prevValue = value;
}
};
}();
```
IIFE的其他應用在本書其他地方有被提及:
* 避免全局變量:從全局范圍中隱藏變量(請參閱最佳實踐:[避免創建全局變量](第16章))
* 創建新的環境:避免共享(參見[陷阱:不小心共享環境](第16章))
* 將全局數據私有到所有構造函數(請參見[將全局數據私有到所有構造函數](第17章對象和繼承.md))
* 將全局數據附加到單例對象上(請參閱[將私有全局數據附加到單例對象](第17章對象和繼承.md))
* 將全局數據附加到方法上(參見[將全局數據附加到方法](第17章對象和繼承.md))
## 全局變量
包含整個程序的作用域分為全局作用域或程序作用域。這是進入一個腳本時的你所處的作用域(無論`<script>`是網頁中的標簽還是一個`.js`文件)。在全局作用域內,您可以通過定義一個函數來創建一個內部的作用域。在這樣的函數中,您可以再次嵌套作用域。每個作用域中都可以訪問其自身的變量以及其周圍的作用域中的變量。由于全局作用域包含了所有其他作用域,它的變量可以隨處訪問:
```js
// here we are in global scope
var globalVariable = 'xyz';
function f() {
var localVariable = true;
function g() {
var anotherLocalVariable = 123;
// All variables of surround scopes are accessible
localVariable = false;
globalVariable = 'abc';
}
}
// here we are again in global scope
```
### 最佳實踐:避免創建全局變量
全局變量有兩個缺點。首先,依賴于全局變量的軟件會受到副作用; 它們不太健壯,行為不太可預測,并且不太可重用。
其次,一個web頁面上的所有JavaScript共享相同的全局變量:您的代碼,內置函數,分析代碼,社交媒體按鈕等。這意味著名稱沖突可能成為一個問題。這就是為什么最好從全球作用域中盡可能多地隱藏變量。例如,不要這樣做:
```html
<!-- Don’t do this -->
<script>
// Global scope
var tmp = generateData();
processData(tmp);
persistData(tmp);
</script>
```
該變量`tmp`變為全局變量,因為它的聲明在全局作用域內執行。但它只在本地使用。因此,我們可以使用IIFE(通過IIFE引入新的范圍)將其隱藏在內部的作用域內:
```js
<script>
(function () { // open IIFE
// Local scope
var tmp = generateData();
processData(tmp);
persistData(tmp);
}()); // close IIFE
</script>
```
### 模塊系統可以減少全局變量的引入
幸運的是,模塊系統(參見[模塊系統](31. 模塊系統和包管理器.md))主要消除了全局變量的問題, 因為模塊不通過全局作用域進行接口,因為每個模塊都有它自己的模塊全局變量。
## 全局對象
*ECMAScript規范* 使用內部數據結構環境來存儲變量(請參閱[環境:變量的管理](####))。該語言有一種不同尋常的特性,通過一個對象,即所謂的全局對象,提供可供訪問全局變量的環境。全局對象可用于創建,讀取和更改全局變量。在全局作用域中,`this`指向它:
```js
> var foo ='hello';
> this.foo //讀取全局變量
'hello'
> this.bar ='world'; //創建全局變量
> bar'world
```
請注意,全局對象具有原型。如果要列出所有(自己的和繼承的)屬性,您需要一個函數,例如`getAllPropertyNames()`[列出所有屬性鍵](##第17章):
```js
> getAllPropertyNames(window).sort().slice(0, 5)
[ 'AnalyserNode', 'Array', 'ArrayBuffer', 'Attr', 'Audio' ]
```
JavaScript創建者Brendan Eich認為全局對象是他[最大的遺憾](https://mail.mozilla.org/pipermail/es-discuss/2013-September/033803.html)之一。它會對性能產生負面影響,使變量作用域的實現更加復雜,并切使模塊化代碼減少。
### 跨平臺注意事項
瀏覽器和Node.js具有用于引用全局對象的全局變量。不幸的是,它們是不同的:
* 瀏覽器包括`window`,作為文檔對象模型(DOM)的一部分被標準化,而不是ECMAScript 5的一部分。每個`frame`或`window`有一個全局對象。
* Node.js包含`global`,一個Node.js特定的變量。每個模塊都有自己的作用域,其中this指向具有該作用域變量的對象。因此,在模塊中`this`和`global`是不同的。
在這兩個平臺上,`this`指的是全局對象,但只有在全局范圍內。在`Node.js`上幾乎不會這樣。如果要以跨平臺方式訪問全局對象,可以使用以下模式:
```js
(function (glob) {
// glob指向全局對象
}(typeof window !== 'undefined' ? window : global));
```
從現在起,我`window`用來引用全局對象,但是在跨平臺代碼中,應該使用前面的模式,就可以使用`glob`。
### window的使用場景
本節介紹通過`window`訪問全局變量的用例。但一般規則是:盡可能避免這樣做。
#### 用例:標記全局變量
前綴`window`是一個可視化的線索,提示代碼指的是全局變量,而不是本地變量:
```js
var foo = 123;
(function () {
console.log(window.foo); // 123
}());
```
但是,這使您的代碼變脆。一旦`foo`從全球作用域轉移到另一個周邊作用域,它就會出問題:
```js
(function () {
var foo = 123;
console.log(window.foo); // undefined
}());
```
因此,最好將`foo`作為一個變量,而不是`window`的屬性。如果你想讓它顯而易見的`foo`是一個全局或全局變量,你可以添加一個名字前綴,如`g_`:
```js
var g_foo = 123;
(function () {
console.log(g_foo);
}());
```
#### 用例:內置的
我不喜歡通過`window`來引用內置的全局變量。它們真是太被人所熟悉了,所以你從一個全局的指示符中幾乎得不到有用的東西。并且加了前綴`window`更加混亂了:
```js
window.isNaN(...) // no
isNaN(...) // yes
```
#### 用例:代碼風格樣式檢查器
當您使用樣式檢查工具(如`JSLint`和`JSHint`)時,使用的window方法時,表示在當前文件中引用未聲明的全局變量時不會收到錯誤。然而,這兩種工具都提供了一些方法來判斷這些變量并避免此類錯誤(可以在他們的文檔中搜索“全局變量”)。
#### 用例:檢查是否存在全局變量
這不是一個常見的用例,但是`shims`和`polyfills`(請參閱[Shims與Polyfills](第30章#shim_vs_polyfill))需要檢查是否存在全局變量`someVariable`。在這種情況下,可以使用``window``:
```js
if (window.someVariable) { ... }
```
這是執行此檢查的安全方法。如果`someVariable`未聲明,以下語句將拋出異常:
```js
// Don’t do this
if (someVariable) { ... }
```
有兩種其他方式可以通過`window`檢查; 它們大致相當,但更明確一些:
```js
if (window.someVariable !== undefined) { ... }
if ('someVariable' in window) { ... }
```
檢查變量是否存在(并具有值)的一般方法是通過`typeof`(參見[typeof:判斷原始值](第9章運算符.md)):
```js
if (typeof someVariable !== 'undefined') { ... }
```
#### 用例:在全局作用域內創建事物
`window` 允許您將事物添加到全局作用域(即使您在一個嵌套的范圍內),并且它允許您有條件地進行:
```js
if (!window.someApiFunction) {
window.someApiFunction = ...;
}
```
通常最好通過`var`添加事物到全局作用域內。但是,通過`window`有條件地添加事物更加簡潔。
## 環境:變量的管理
| 提示 |
| :---: |
| 環境是一個高級話題。它們是JavaScript內部的細節。如果您想更深入地了解變量的工作原理,請閱讀本節。 |
當程序執行進入它們的作用域時,變量就會出現。然后它們需要存儲空間。**提供存儲空間的數據結構稱為JavaScript中的環境**。它將變量名稱映射到值。其結構與JavaScript對象非常相似。環境有時會在你離開它們的作用域之后繼續存在。因此,它們存儲在堆上,而不是堆棧。
變量以兩種方式傳遞。它們有兩個維度,下面介紹:
* 動態維度:調用函數
每次調用一個函數時,它的參數和變量都需要新的存儲。完成后,通常可以回收該存儲。例如,采用階乘函數的以下實現。它自動遞歸調用多次,每次都需要為`n`創建新的存儲空間:
```js
function fac(n) {
if (n <= 1) {
return 1;
}
return n * fac(n - 1);
}
```
* 詞匯(靜態)維度:函數可以維持其周圍的作用域
不管函數被調用的多頻繁,它總是需要訪問它自己的(新的)局部變量和周圍作用域的變量。例如,下面的函數,在`doNTimes`中有一個輔助函數`doNTimesRec`。當`doNTimesRec`多次調用自己時,每次都會創建一個新的環境。然而,在這些調用期間,`doNTimesRec`也會與`doNTimes`的單一環境維持聯系(類似于所有函數共享一個單一的全局環境)。因為`doNTimesRec`需要連接以訪問(1)中的`action`:
```js
function doNTimes(n, action) {
function doNTimesRec(x) {
if (x >= 1) {
action(); // (1)
doNTimesRec(x-1);
}
}
doNTimesRec(n);
}
```
這兩個維度的處理方式如下:
* 動態維度:執行上下文的堆棧
每次調用函數時,都會創建一個新的環境,將標識符(參數和變量)映射到值。為了處理遞歸,*執行上下文* - 引用環境 - 在堆棧中進行管理。該堆棧反映了調用堆棧。
* 詞匯維度:環境鏈
為了支持這個維度,函數通過內部屬性`[[Scope]]`來記錄它所創建的作用域。調用函數時,將為進入的那個新作用域創建一個環境。這個環境通過`[[Scope]]`屬性使`outer`字段指向了外部作用域的環境。因此,始終存在一系列環境,從當前活躍的環境開始,繼續其外部環境,等等。每個鏈都以全局環境(所有初始調用的函數的作用域)結束。字段`outer`指向的全局環境是`null`。
要解析一個標識符,從活動環境開始,將遍歷整個環境鏈。
我們來看一個例子:
```js
function myFunction(myParam) {
var myVar = 123;
return myFloat;
}
var myFloat = 1.3;
// Step 1
myFunction('abc'); // Step 2
```

*圖:變量的動態維度是通過一組執行上下文來處理的,靜態維度是由鏈接環境處理的。活動的執行上下文、環境和函數被突出顯示。
Step1 在函數調用`myFunction(abc)`之前顯示這些數據結構。
Step2 在函數調用期間顯示它們。*
關于上圖,說明在執行前面的代碼時發生了什么
1. `myFunction`和`myFloat`被存儲在全局環境中(#0)
注意,`myFunction`引用的`function`對象通過內部屬性`[[Scope]]`指向其作用域(全局作用域)
2. 為了執行`myFunction('abc')`,創建了一個保存參數和局部變量的新的全局作用賦(#1)。它是通過`outer`指向了外部環境(它是由`myFunction.[[Scope]]`初始化的)。因為外部環境,`myFunction`才可以訪問`myFloat`。
## 閉包:使得函數可以維持其創建時所在的作用域
如果一個函數離開創建它的作用域,可以維持創建時(以及周圍范圍)作用域中的變量。例如:
```js
function createInc(startValue) {
return function (step) {
startValue += step;
return startValue;
};
}
```
返回的函數`createInc()`不會失去與`startValue`的連接—在函數調用時,該變量使得函數一直處于持久的狀態:
```js
> var inc = createInc(5);
> inc(1)
6
> inc(2)
8
```
**閉包**是一個函數附加了可以鏈接該函數被創建時的作用域的鏈接。這個名字來源于事實,即一個閉包“封閉”函數的自由變量。如果一個變量沒有在函數中聲明,那么這個變量是自由的,也就是說,如果它“從外部”來。
### 通過環境來控制閉包
| 提示 |
|:----:|
| 這是一個進一步深入了解閉包工作的高級部分。 您應該熟悉環境(參考[環境:管理變量](###))。 |
閉包是一個在執行之后仍然存在的環境的示例。為了說明閉包的工作原理,我們先來看看之前的交互過程`createInc()`,并將其分解成四個步驟(在每個步驟中,活動執行上下文和它的環境被突出顯示;如果一個函數是活動的,它也被突出顯示):
1. 這個步驟在交互之前進行,并在函數聲明的解析之后進行`createInc`。`createInc`條目已被添加到全局環境(#0)并指向一個函數對象。

2. 這個步驟發生在函數調用`createInc(5)`的執行過程中。為`createInc`創建一個新的環境(#1),并將其推入堆棧。它的外部環境是全局環境(與`createInc.[[Scope]]`相同)。環境保存參數`startValue`。

3. 這個步驟是在賦值給`inc`之后發生的。在我們從`createInc`返回之后,指向其環境的執行上下文從堆棧中刪除,但是環境仍然存在于堆中,因為`inc.[[Scope]]`指向了它。`inc`是一個閉包(一個附加出生環境的函數)。

4. 這一步發生在`inc(1)`的執行過程中。一個新的環境(#1)已經被創建,一個指向它的執行上下文被推到堆棧上。它的外部環境是`inc`的 `[[Scope]]`,外部環境使`inc`能夠訪問`startValue`。

5. 這一步是在`inc(1)`執行之后發生的。沒有任何引用(執行上下文、`outer`字段或`[[Scope]]`)指向`inc`的環境。因此不需要它,可以從堆中刪除。

### 陷阱:不經意間的環境共用
有時,您創建的函數的行為受到當前作用域中的變量的影響。在JavaScript中,這可能是有問題的,因為每個函數都應該處理變量在創建函數時所具有的值。然而,由于函數是閉包的,函數將始終與變量的當前值一起工作。在`for`循環中,事情就不會按預期工作。通過下面的例子可以看的更清楚:
```js
function f() {
var result = [];
for (var i=0; i<3; i++) {
var func = function () {
return i;
};
result.push(func);
}
return result;
}
console.log(f()[1]()); // 3
```
`f`返回一個包含三個函數的數組。所有這些函數仍然可以訪問`f`的環境和`i`。事實上,它們共享相同的環境。唉,在循環完成之后,在那個環境中`i`的值為3。因此,所有函數都返回3。
這不是我們想要的。為了解決問題,我們需要在創建使用該函數的函數之前對索引進行快照。換句話說,我們想用函數的創建時的值來包裝每個函數。因此,我們將采取以下步驟:
1. 為返回的數組中的每個函數創建一個新環境。
2. 存儲(一個拷貝)當前`i`在那時環境中的值
只有函數才創建環境,所以我們使用IIFE(請參閱[通過IIFE引入新的作用域](###))來完成步驟1:
```js
function f() {
var result = [];
for (var i=0; i<3; i++) {
(function () { // step 1: IIFE
var pos = i; // step 2: copy
var func = function () {
return pos;
};
result.push(func);
}());
}
return result;
}
console.log(f()[1]()); // 1
```
注意, 該示例具有實際意義, 因為當通過循環將事件處理程序添加到DOM元素時會出現類似的情況。
- 本書簡介
- 前言
- 關于這本書你需要知道些什么
- 如何閱讀本書
- 目錄
- I. JavaScript的快速入門
- 第1章 基礎的JavaScript
- II. 背景知識
- 第2章 為什么選擇JavaScript?
- 第3章 JavaScript的性質
- 第4章 JavaScript是如何創造出來的
- 第5章 標準化:ECMAScript
- 第6章 JavaScript的歷史里程碑
- III. 深入JavaScript
- 第7章 JavaScript語法
- 第8章 值
- 第9章 運算符
- 第10章 布爾值
- 第11章 數字
- 第12章 字符串
- 第13章 語句
- 第14章 異常捕獲
- 第15章 函數
- 第16章 變量:作用域、環境和閉包
- 第17章 對象和繼承
- 第18章 數組
- 第19章 正則表達式
- 第20章 Date
- 第21章 Math
- 第22章 JSON
- 第23章 標準全局變量
- 第24章 編碼和JavaScript
- 第25章 ECMAScript 5中的新功能
- IV. 技巧、工具和類庫
- 第26章 元代碼樣式指南
- 第27章 調試的語言機制
- 第28章 子類化內置構造函數
- 第29章 JSDoc:生成API文檔
- 第30章 類庫
- 第31章 模塊系統和包管理器
- 第32章 其他工具
- 第33章 接下來該做什么
- 著作權