## 10 變量和賦值
> 原文: [http://exploringjs.com/impatient-js/ch_variables-assignment.html](http://exploringjs.com/impatient-js/ch_variables-assignment.html)
>
> 貢獻者:[so-hard](https://github.com/so-hard)
下面這些是 JavaScript 聲明變量的主要方式:
* `let`用來聲明可變變量。
* `const`用來聲明常量(不可變變量)。
在ES6之前,還有`var`。但它有些怪癖,所以最好在現代JavaScript程序中避免使用它。你可以在[“Speaking JavaScript”](http://speakingjs.com/es5/ch16.html)中閱讀更多相關信息。
### 10.1 `let`
通過`let`聲明變量的值是可變的:
```js
let i;
i = 0;
i = i + 1;
assert.equal(i, 1);
````
你也可以聲明變量后緊接著賦值:
```js
let i = 0
```
### 10.2 `const`
通過`const`聲明的變量是不可變的。你必須立即給它初始化(賦值):
```js
const i = 0;// 必須立即初始化
assert.throws({
name: 'TypeError',
message: 'Assignment to constant variable.',//賦值給常量
})
```
#### 10.2.1 `const`和不可變性
在JavaScript中,`const`只是表示這種*binding*(變量名和值之間的關聯)是不可變。值本身可能是可變,如下例中的`obj`。
```js
const obj = { prop: 0 };
//允許: obj的prop屬性值改變
obj.prop = obj.prop + 1;
assert.equal(obj.prop, 1);
// 不允許: 給obj賦值
assert.throws(
() => { obj = {} },
{
name: 'TypeError',
message: 'Assignment to constantvariable.',
}
);
```
#### 10.2.2 `const`和循環
你可以將`const`與`for-of`循環一起使用,每次迭代都會有一個新的綁定被創建:
```js
const arr = ['hello', 'world'];
for (const elem of arr) {
console.log(elem);
}
// Output:
// 'hello'
// 'world'
```
在普通的`for`循環中,必須使用`let`,however:
```js
const arr = ['hello', 'world'];
for (let i=0; i<arr.length; i++) {
const elem = arr[i];
console.log(elem);
}
```
### 10.3 在`let`和`const`之間做出決定
我推薦按以下規則來決定使用`let`或是`const`:
* `const`意味著固定的綁定,變量的值永遠不會改變。用const就沒錯了。
* `let`表示變量的值發生變化。僅在不能使用const時才使用它。
 **練習:`const`**
`exercises/variables-assignment/const_exrc.js`
### 10.4 變量的作用域
變量的作用域是指該變量可以訪被程序的區域。請思考下面代碼。
```js
{ // // Scope A. x(直接作用域)可被訪問
const x = 0;
assert.equal(x, 0);
{ // Scope B. x, y可被訪問
const y = 1;
assert.equal(x, 0);
assert.equal(y, 1);
{ // Scope C. x, y, z可被訪問
const z = 2;
assert.equal(x, 0);
assert.equal(y, 1);
assert.equal(z, 2);
}
}
}
//作用域外. x, y, z不可被訪問
assert.throws(
() => console.log(x),
{
name: 'ReferenceError',
message: 'x is not defined',
}
);
```
* 作用域A是`x`的直接作用域。
* 作用域B和C在嵌套在作用域A的內部。
* 作用域A在作用域B和C的外面。
每個變量都可以被訪問在它的直接作用域以及嵌套在它(直接作用域)內部的作用域。
通過const和let聲明的變量稱為塊作用域,因為它們的作用域涵蓋是最內層的塊。
#### 10.4.1。遮蔽效應和塊
你不能聲明兩次相同的變量在同一層級:
```js
assert.throws(
() => {
eval('let x = 1; let x = 2;');
},
{
name: 'SyntaxError',
message: "Identifier 'x' has already been declared",
});
```
```js
assert.throws(
() => {
eval('let x = 1; let x = 2;');
},
{
name: 'SyntaxError',
message: "Identifier 'x' has already been declared",
});
```
 **為什么`eval()`?**
我們需要通過 eval()延遲解析,否則我們在解析此代碼時會遇到異常。 `assert.throws()`僅在其函數體內拋出異常時才有效。
你可以`x`的塊中嵌套一個塊,并可以定義與外層塊同名的變量:
```js
const x = 1;
assert.equal(x, 1);
{
const x = 2;
assert.equal(x, 2);
}
assert.equal(x, 1);
```
在內層塊中,`x`是唯一具有該名稱的可訪問變量。內部`x`遮蔽外層`x`。離開塊后,外層的`x`可以被訪問。
 **測驗:基本**
參見[測驗應用程序](ch_quizzes-exercises.html#quizzes)。
### 10.5。 (高級)
所有剩余部分都是高級的。
### 10.6。術語:靜態的與動態的
這兩個形容詞描述了編程語言中的現象:
* 靜態: 某些內容與源碼有關,在不執行代碼的情況下確定。
* 動態: 表示運行時。
我們來看下面兩個例子。
#### 10.6.1。靜態現象:變量的作用域
變量作用域是一種靜態現象。請思考以下代碼:
```js
function f() {
const x = 3;
// ···
}
```
`x`是靜態 (詞法)作用域。也就是說,它的作用域是固定的,而且在運行時不會改變。
變量作用域成靜態樹(通過靜態嵌套)。
#### 10.6.2。動態現象:函數調用
函數調用是一種動態現象。思考以下代碼:
```js
function g(x) {}
function h(y) {
if (Math.random()){// (A)
g(y);
}
}
```
函數是否被調用取決的A行,只能在運行時確定。
函數的調用形成動態樹(通過動態調用)。
### 10.7 "暫時性死區”:避免訪問未聲明的變量
對于 JavaScript,TC39 ( 指的是技術委員會(Technical Committee)第39 號。它是ECMA 的一部分,ECMA 是“ECMAScript” 規范下的JavaScript 語言標準化的機構。)需要決定,如果在聲明變量之前訪問其直接作用域中的變量會發生什么:
```js
{
console.log(x); // 這里將會打印什么
let x;
}
```
一些可能的方法是:
1. 該變量被解析在當前作用域的作用域范圍內。
2. 如果讀取變量,你會得到`undefined`。你也可以給變量賦值。 (這就是`var`的工作原理。)
3. 拋出錯誤。
TC39 為`const`和`let`選擇了(3),因為你可能犯錯了,才會使用一個未申明的變量。 (2)不適用于`const`(其中每個變量應該具有初始化的值)。因此,`let`也被拒絕,因此兩者的工作方式相似,并且很容易在它們之間切換。
在進入變量的作用域到變量的聲明這段時間“暫時性死區”(TDZ):
* 在此期間,變量被認為是未初始化的(就好像它是一個特殊值)。
* 如果訪問未聲明的變量,則會得到`ReferenceError`。
* 到變量聲明后,變量獲得初始值(通過賦值符號指定)或`undefined` - 如果沒有賦初值。
以下代碼表明了了“暫時性死區”(TDZ):
```js
if (true) { // `tmp`作用域的起點, TDZ starts
// `tmp` 未被初始化:
assert.throws(() => (tmp = 'abc'), ReferenceError);
assert.throws(() => console.log(tmp), ReferenceError);
let tmp; // TDZ ends
assert.equal(tmp, undefined);
}
```
接下來這個例子說明了“暫時性死區”是“暫時的”(與調用時間有關):
```js
if (true) { // `myVar`作用域起點, TDZ starts
const func = () => {
console.log(myVar); // 稍后執行
};
// TDZ 內部:
// 訪問 `myVar` 會拋出 `ReferenceError`
let myVar = 3; // TDZ 結束
func(); // OK, 在 TDZ外調用
}
```
即使`func()`位于`myVar`聲明之前并使用該變量,我們也可以調用`func()`。但是我們必須等到`myVar`的暫時死區結束。
### 10.8 提升
提升意味著一個構件會移動到作用域啟始位置,而不管它位于作用域哪個位置:
```js
assert.equal(func(), 123); // Works!
function func() {
return 123;
}
```
您可以在聲明之前使用`func()`,因為在作用域內它會被提升。也就是說,上面的代碼實際上是這樣執行的:
```js
function func() {
return 123;
}
assert.equal(func(), 123);
```
暫時性死區可以被視為提升的一種,因為聲明會影響它的作用域起始發生什么。
**有關提升功能的更多信息**
有關提升如何影響功能的更多信息,請參閱[回調值](ch_callables.html#hoisting-functions)的章節。
### 10.9全局變量
如果一個變量聲明在頂級作用域,那么它就是全局變量。每個內層作用域都可以訪問全局變量。在JavaScript中,會有多個全局作用域(圖 [5](#fig:global-scopes) ):
* 最外層的全局作用域是特殊的:它的變量可以通過對象(全局對象)的屬性所被訪問。全局對象在瀏覽器環境中被稱為window和self。此作用域的變量通過以下方式創建的:
* 作為全局對象的屬性
* `var`和`function`在腳本的最上面聲明的變量(*script*被瀏覽器支持。它們是簡單的代碼片段以及模塊的前身。有關詳細信息,請參閱[模塊](ch_modules.html#scripts)的章節。)
* 嵌套在這個作用域內是腳本的全局作用域。此作用域中的變量由`let`,`const`和`class`在腳本的最上面聲明。
* 嵌套在該作用域的是模塊的作用域。每個模塊都有自己的全局作用域。變量在該作用域被最上面的模塊聲明。

Figure 5: JavaScript有多個全局作用域。
#### 10.9.1。全局對象
全局對象允許您通過對象訪問最外層的作用域。他們倆總是同步的:
* 如果在最外層的作用域中創建變量,則全局對象將獲取新屬性。如果更改這個全局變量,那么這個屬性也會更改。
* 如果你創建或刪除全局對象的屬性,那么相應的也會創建或刪除相應的全局變量。如果更改全局對象的屬性,則相應的全局變量將更改。
全局對象可通過特殊變量獲得:
* `window`:是引用全局對象的經典方式。但它只適用于普通的瀏覽器,不適用于*Node.js*和*Web Workers*(與瀏覽器代碼同時運行的進程;有關詳細信息,請參閱[關于異步編程的章節](ch_async-js.html#web-workers))。
* `self`:在瀏覽器環境中隨處可用,包括 Web Workers。但是 Node.js 不支持它。
* `global`:僅在 Node.js 中可用。
讓我們來看看`self`是如何工作的:
```js
// 在腳本的最上面
var myGlobalVariable = 123;
assert.equal('myGlobalVariable' in self, true);
delete self.myGlobalVariable;
assert.throws(() => console.log(myGlobalVariable), ReferenceError);
// 隨便在哪兒創建一個全局變量:
if (true) {
self.anotherGlobalVariable = 'abc';
}
assert.equal(anotherGlobalVariable, 'abc');
```
#### 10.9.2。避免全局對象!
Brendan Eich稱全局對象是他設計JavaScript最大的遺憾之一。不要將變量放在它的作用域:
* 通常,網頁上所有腳本都是全局變量的話造成命名沖突。
* 通過全局對象,你可以在任何地方創建和刪除全局變量。這樣做會使代碼無法預測,因為通常在嵌套作用域中無法進行此類更改。
你有時會在網絡上的教程中看到`window.globalVariable`,但前綴“`window.`”不是必須的。我寧愿省略它:
```js
window.encodeURIComponent(str); // no
encodeURIComponent(str); // yes
```
### 10.10 閉包
在我們探索閉包之前,我們需要了解綁定變量和自由變量。
#### 10.10.1。綁定變量與自由變量
每個范圍,都有一組提到的變量。在這些變量中我們區分:
* 綁定變量: 在作用域內聲明。它們是參數或局部變量。
* 自由變量: 在作用域外聲明。它們也被稱為非局部變量。
請考慮以下代碼:
```js
function func(x) {
const y = 123;
console.log(z);
}
```
在`func()`的主體中,`x`和`y`是綁定變量。 `z`是一個自由變量。
#### 10.10.2 什么是閉包?
什么是閉合呢?
> 閉包是一個函數加上與“出生地”存在的變量的連接。
維持這種連接有什么意義?它讓函數自由變量的值可被訪問。例如:
```js
function funcFactory(value) {
return () => {
return value;
};
}
const func = funcFactory('abc');
assert.equal(func(), 'abc'); // (A)
```
`funcFactory`返回一個閉包賦值給`func`。當func在A行被調用時,它仍然能訪問自由變量的值,因為func跟變量有關聯,(即使不在它的作用域)。

**JavaScript中的所有函數都是閉包**
在JavaScript通過閉包支持靜態作用域。因此,每個函數都是一個閉包。如果你對閉包的工作原理感興趣,請參閱[“可變環境”](ch_remaining-chapters-preview.html)一章(如 ECMAScript 規范中所述)。
#### 10.10.3 示例:incrementors工廠
以下函數返回`incrementors`(作者創造的)。incrementor是內部存儲數字的函數。它被調用后,通過向其添加參數來更新該數字并返回新值。
```js
function createInc(startValue) {
return (step) => { // (A)
startValue += step;
return startValue;
};
}
const inc = createInc(5);
assert.equal(inc(2), 7);
```
我們可以看到在A行創建的函數將其內部數字保存在自由變量`startValue`中。這一次,我們不只在它聲明的作用域中讀取,我們使用它來存儲我們更改的數據以及在函數調用中持續存在的數據
我們可以通過局部變量在它聲明的作用域內創建更多存儲插槽:
```js
function createInc(startValue) {
let index = -1;
return (step) => {
startValue += step;
index++;
return [index, startValue];
};
}
const inc = createInc(5);
assert.deepEqual(inc(2), [0, 7]);
assert.deepEqual(inc(2), [1, 9]);
assert.deepEqual(inc(2), [2, 11]);
```
#### 10.10.4 閉包的應用
閉包的最佳實踐
* 對于初學者來說,它們只是靜態作用域的實現。因此,它們為回調提供上下文數據。
* 函數也可以使用它們來存儲跨函數調用持久存在的狀態。 `createInc()`就是一個例子。
* 并且它們可以為對象提供私有數據(通過字面值或類生成)。有關其工作原理的詳細信息,請參見[“探索 ES6”](http://exploringjs.com/es6/ch_classes.html#_private-data-via-constructor-environments)。
### 10.11 總結:聲明變量的方法
Table 1: 下面是所有聲明變量的方法在JavaScript中。
| | 提升 | 作用域 | Script作用域是全局對象? |
|------------|----------|------|----------------------|
| `var` | 僅聲明 | 函數 | `?` |
| `let` | 時間死區 | 塊級 | `?` |
| `const` | 時間死區 | 塊級 | `?` |
| `function` | 所有 | 塊級 | `?` |
| `class` | 沒有 | 塊級 | `?` |
| `import` | 所有 | 模塊 | `?` |
TBL。 [1](#tbl:declaring-variables) 列出了在 JavaScript 中聲明變量的所有方法:`var`,`let`,`const`,`function`,`class`和`import`。
 **測驗:高級**
參見[測驗應用程序](ch_quizzes-exercises.html#quizzes)。
### 10.12 進一步閱讀
有關變量是如何被引擎處理的更多信息,請參閱[“可變環境”一章](ch_remaining-chapters-preview.html)。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?