20181128——阮一峰ES6閱讀 let與const 變數的解構賦值
let命令
與var類似宣告變數,只不過在在當前的程式碼塊中有效。
for迴圈的計數器,就很適合let命令
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
程式碼中,計數器i只在for迴圈體內有效,再迴圈體外引用就會報錯
i is not defined
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 下面的程式碼如果使用var,最後輸出的是10。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
另外,for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
不存在變數提升
為了糾正這種現象,let命令改變了語法行為,它所宣告的變數一定要在聲明後使用,否則報錯。
// var 的情況 console.log(foo); // 輸出undefined var foo = 2; // let 的情況 console.log(bar); // 報錯ReferenceError let bar = 2;
暫時性死區
只要塊級作用域記憶體在let命令,它所宣告的變數就“繫結”(binding)這個區域,不再受外部的影響。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面程式碼中,存在全域性變數tmp,但是塊級作用域內let又聲明瞭一個區域性變數tmp,導致後者繫結這個塊級作用域,所以在let宣告變數前,對tmp賦值會報錯。
ES6 明確規定,如果區塊中存在let和const命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。
不允許重複宣告
et不允許在相同作用域內,重複宣告同一個變數
// 報錯
function func() {
let a = 10;
var a = 1;
}
// 報錯
function func() {
let a = 10;
let a = 1;
}
因此,不能在函式內部重新宣告引數。
function func(arg) {
let arg; // 報錯
}
function func(arg) {
{
let arg; // 不報錯
}
}
ES6 的塊級作用域——let實際上為 JavaScript 新增了塊級作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函式有兩個程式碼塊,都聲明瞭變數n,執行後輸出 5。這表示外層程式碼塊不受內層程式碼塊的影響。如果兩次都使用var定義變數n,最後輸出的值才是 10。
第一種場景,內層變數可能會覆蓋外層變數。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
第二種場景,用來計數的迴圈變數洩露為全域性變數。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
const 命令
const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
const宣告的變數不得改變值,這意味著,const一旦宣告變數,就必須立即初始化,不能留到以後賦值
const foo;
// SyntaxError: Missing initializer in const declaration
const的作用域與let命令相同:只在宣告所在的塊級作用域內有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
**本質 **
const實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向另一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。
const foo = {};
// 為 foo 新增一個屬性,可以成功
foo.prop = 123;
foo.prop // 123
// 將 foo 指向另一個物件,就會報錯
foo = {}; // TypeError: "foo" is read-only
上面程式碼中,常量foo儲存的是一個地址,這個地址指向一個物件。不可變的只是這個地址,即不能把foo指向另一個地址,但物件本身是可變的,所以依然可以為其新增新屬性。
下面是另一個例子。
const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯
ES6 宣告變數的六種方法
ES5 只有兩種宣告變數的方法:var命令和function命令。ES6 除了新增let和const命令,後面章節還會提到,另外兩種宣告變數的方法:import命令和class命令。所以,ES6 一共有 6 種宣告變數的方法。
頂層物件的屬性
頂層物件,在瀏覽器環境指的是window物件,在 Node 指的是global物件。ES5 之中,頂層物件的屬性與全域性變數是等價的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面程式碼中,頂層物件的屬性賦值與全域性變數的賦值,是同一件事。
頂層物件的屬性與全域性變數掛鉤,被認為是 JavaScript 語言最大的設計敗筆之一。這樣的設計帶來了幾個很大的問題,首先是沒法在編譯時就報出變數未宣告的錯誤,只有執行時才能知道(因為全域性變數可能是頂層物件的屬性創造的,而屬性的創造是動態的);其次,程式設計師很容易不知不覺地就建立了全域性變數(比如打字出錯);最後,頂層物件的屬性是到處可以讀寫的,這非常不利於模組化程式設計。另一方面,window物件有實體含義,指的是瀏覽器的視窗物件,頂層物件是一個有實體含義的物件,也是不合適的。
ES6 為了改變這一點,一方面規定,為了保持相容性,var命令和function命令宣告的全域性變數,依舊是頂層物件的屬性;另一方面規定,let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。也就是說,從 ES6 開始,全域性變數將逐步與頂層物件的屬性脫鉤。
var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者採用通用方法,寫成 this.a
window.a // 1
let b = 1;
window.b // undefined
變數的解構賦值
陣列的解構賦值
ES6 允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構(Destructuring)。 **ES6 允許寫成下面這樣。** let [a, b, c] = [1, 2, 3];
let [a, b, c] = [1, 2, 3];
上面程式碼表示,可以從陣列中提取值,按照對應位置,對變數賦值。
本質上,這種寫法屬於“模式匹配”,只要等號兩邊的模式相同,左邊的變數就會被賦予對應的值。下面是一些使用巢狀陣列進行解構的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
如果解構不成功,變數的值就等於undefined。
let [foo] = [];
let [bar, foo] = [1];
另一種情況是不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的陣列。這種情況下,解構依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
上面兩個例子,都屬於不完全解構,但是可以成功。
如果等號的右邊不是陣列(或者嚴格地說,不是可遍歷的結構,參見《Iterator》一章),那麼將會報錯。
事實上,只要某種資料結構具有 Iterator 介面,都可以採用陣列形式的解構賦值。
function* fibs() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5
解構賦值允許指定預設值。
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
物件的解構賦值
解構不僅可以用於陣列,還可以用於物件。let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
物件的解構與陣列有一個重要的不同。陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
上面程式碼的第一個例子,等號左邊的兩個變數的次序,與等號右邊兩個同名屬性的次序不一致,但是對取值完全沒有影響。第二個例子的變數沒有對應的同名屬性,導致取不到值,最後等於undefined。
如果變數名與屬性名不一致,必須寫成下面這樣。
這實際上說明,物件的解構賦值是下面形式的簡寫(參見《物件的擴充套件》一章)。
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };