這里我僅僅是記錄了那些我認為值得注意的ES6知識點,詳細版請挪步https://es6.ruanyifeng.com/#docs/let
let和const命令
let
聲明的變量只在它所在的代碼塊有效。
var a = [];
for (let i = 0; i < 10; i++) {a[i] = function () {console.log(i);};
}
a[6](); // 6
上面代碼中,變量i
是let
聲明的,當前的i
只在本輪循環有效,所以每一次循環的i
其實都是一個新的變量,所以最后輸出的是6
。你可能會問,如果每一輪循環的變量i
都是重新聲明的,那它怎么知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i
時,就在上一輪循環的基礎上進行計算。
另外,for
循環還有一個特別之處,就是在條件中設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。(感覺實現機制是閉包,將參數都初始化之后return一個函數)(我想因為在for中初始化的變量只會執行一次并隨后記錄在引擎中,所以for循環內部聲明的變量與for條件中的變量并不會產生沖突,這里我的想法是錯誤的,原因請看下去)
for (let i = 0; i < 3; i++) {let i = 'abc';console.log(i);
}
// abc
// abc
// abc
上面代碼正確運行,輸出了 3 次abc
。這表明函數內部的變量i
與循環變量i
不在同一個作用域,有各自單獨的作用域。
不存在變量提升
let所聲明的變量一定要在聲明后使用,否則報錯。
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
變量bar
用let
命令聲明,不會發生變量提升。這表示在聲明它之前,變量bar
是不存在的,這時如果用到它,就會拋出一個錯誤。
暫時性死區
只要塊級作用域內存在let
命令,它所聲明的變量就“綁定”(binding)這個區域,不再受外部的影響。
var tmp = 123;if (true) {tmp = 'abc'; // ReferenceErrorlet tmp;
}
上面代碼中,存在全局變量tmp
,但是塊級作用域內let
又聲明了一個局部變量tmp
,導致后者綁定這個塊級作用域,所以在let
聲明變量前,對tmp
賦值會報錯。
ES6 明確規定,如果區塊中存在let
和const
命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。
總之,在代碼塊內,使用let
命令聲明變量之前,該變量都是不可用的。這在語法上,稱為“暫時性死區”(temporal dead zone,簡稱 TDZ)。
if (true) {// TDZ開始tmp = 'abc'; // ReferenceErrorconsole.log(tmp); // ReferenceErrorlet tmp; // TDZ結束console.log(tmp); // undefinedtmp = 123;console.log(tmp); // 123
}
上面代碼中,在let
命令聲明變量tmp
之前,都屬于變量tmp
的“死區”。
“暫時性死區”也意味著typeof
不再是一個百分之百安全的操作。
typeof x; // ReferenceError
let x;
上面代碼中,變量x
使用let
命令聲明,所以在聲明之前,都屬于x
的“死區”,只要用到該變量就會報錯。因此,typeof
運行時就會拋出一個ReferenceError
。
作為比較,如果一個變量根本沒有被聲明,使用typeof
反而不會報錯。
typeof undeclared_variable // "undefined"
上面代碼中,undeclared_variable
是一個不存在的變量名,結果返回“undefined”。所以,在沒有let
之前,typeof
運算符是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的編程習慣,變量一定要在聲明之后使用,否則就報錯。
有些“死區”比較隱蔽,不太容易發現。
function bar(x = y, y = 2) { //語句從左到右按順序執行,一個, 分隔兩個語句return [x, y];
}bar(); // 報錯
上面代碼中,調用bar
函數之所以報錯(某些實現可能不報錯),是因為參數x
默認值等于另一個參數y
,而此時y
還沒有聲明,屬于“死區”。如果y
的默認值是x
,就不會報錯,因為此時x
已經聲明了。( 這里會報錯我猜由于此刻參數是存在默認值的,如果你調用函數的時候沒有傳參,那么就會使用默認值,此刻(x = y)執行,但是y還沒有被聲明賦值過,所以會報錯,而下面的例子里,x = 2已經聲明賦值過了,所以執行 y = x 的時候能正確執行)
function bar(x = 2, y = x) {return [x, y];
}
bar(); // [2, 2]
另外,下面的代碼也會報錯,與var
的行為不同。
// 不報錯
var x = x;// 報錯
let x = x;
// ReferenceError: x is not defined
上面代碼報錯,也是因為暫時性死區。使用let
聲明變量時,只要變量在還沒有聲明完成前使用,就會報錯。上面這行就屬于這個情況,在變量x
的聲明語句還沒有執行完成前,就去取x
的值,導致報錯”x 未定義“。(一個語句的執行順序是從右往左,所以x為聲明而報錯)
ES6 規定暫時性死區和let
、const
語句不出現變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。這樣的錯誤在 ES5 是很常見的,現在有了這種規定,避免此類錯誤就很容易了。
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。
不允許重復聲明
let
不允許在相同作用域內,重復聲明同一個變量。
// 報錯
function func() {let a = 10;var a = 1;
}// 報錯
function func() {let a = 10;let a = 1;
}
因此,不能在函數內部重新聲明參數。(正好對應上面的for循環中的父作用域與子作用域的解說!)
function func(arg) {let arg;
}
func() // 報錯function func(arg) {{let arg;}
}
func() // 不報錯
ES6 的塊級作用域
let
實際上為 JavaScript 新增了塊級作用域。
function f1() {let n = 5;if (true) {let n = 10;}console.log(n); // 5
}
上面的函數有兩個代碼塊,都聲明了變量n
,運行后輸出 5。這表示外層代碼塊不受內層代碼塊的影響。如果兩次都使用var
定義變量n
,最后輸出的值才是 10。
ES6 允許塊級作用域的任意嵌套。
{{{{{let insane = 'Hello World'}console.log(insane); // 報錯
}}}};
上面代碼使用了一個五層的塊級作用域,每一層都是一個單獨的作用域。第四層作用域無法讀取第五層作用域的內部變量。
內層作用域可以定義外層作用域的同名變量。
{{{{let insane = 'Hello World';{let insane = 'Hello World'}
}}}};
塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執行函數表達式(匿名 IIFE)不再必要了。
// IIFE 寫法
(function () {var tmp = ...;...
}());// 塊級作用域寫法
{let tmp = ...;...
}
塊級作用域與函數聲明
函數能不能在塊級作用域之中聲明?這是一個相當令人混淆的問題。
ES5 規定,函數只能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。
// 情況一
if (true) {function f() {}
}// 情況二
try {function f() {}
} catch(e) {// ...
}
上面兩種函數聲明,根據 ES5 的規定都是非法的。
但是,瀏覽器沒有遵守這個規定,為了兼容以前的舊代碼,還是支持在塊級作用域之中聲明函數,因此上面兩種情況實際都能運行,不會報錯。
ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數。ES6 規定,塊級作用域之中,函數聲明語句的行為類似于let
,在塊級作用域之外不可引用。
function f() { console.log('I am outside!'); }(function () {if (false) {// 重復聲明一次函數ffunction f() { console.log('I am inside!'); }}f();
}());
上面代碼在 ES5 中運行,會得到“I am inside!”,因為在if
內聲明的函數f
會被提升到函數頭部,實際運行的代碼如下。
// ES5 環境
function f() { console.log('I am outside!'); }(function () {function f() { console.log('I am inside!'); }if (false) {}f();
}());
ES6 就完全不一樣了,理論上會得到“I am outside!”。因為塊級作用域內聲明的函數類似于let
,對作用域之外沒有影響。但是,如果你真的在 ES6 瀏覽器中運行一下上面的代碼,是會報錯的,這是為什么呢?
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }(function () {if (false) {// 重復聲明一次函數ffunction f() { console.log('I am inside!'); }}f();
}());
// Uncaught TypeError: f is not a function
上面的代碼在 ES6 瀏覽器中,都會報錯。
原來,如果改變了塊級作用域內聲明的函數的處理規則,顯然會對老代碼產生很大影響。為了減輕因此產生的不兼容問題,ES6 在附錄 B里面規定,瀏覽器的實現可以不遵守上面的規定,有自己的行為方式。
- 允許在塊級作用域內聲明函數。
- 函數聲明類似于
var
,即會提升到全局作用域或函數作用域的頭部。 - 同時,函數聲明還會提升到所在的塊級作用域的頭部。(是函數聲明還是整個函數都提升到塊級作用域?)
注意,上面三條規則只對 ES6 的瀏覽器實現有效,其他環境的實現不用遵守,還是將塊級作用域的函數聲明當作let
處理。
根據這三條規則,瀏覽器的 ES6 環境中,塊級作用域內聲明的函數,行為類似于var
聲明的變量。上面的例子實際運行的代碼如下。
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {var f = undefined; // 函數聲明類似于var,即會提升到全局作用域或函數作用域的頭部if (false) {function f() { console.log('I am inside!'); } //同時,函數還會提升到所在的塊級作用域的頭部。}f();
}());
// Uncaught TypeError: f is not a function
考慮到環境導致的行為差異太大,應該避免在塊級作用域內聲明函數。如果確實需要,也應該寫成函數表達式,而不是函數聲明語句。
// 塊級作用域內部的函數聲明語句,建議不要使用
{let a = 'secret';function f() {return a;}
}// 塊級作用域內部,優先使用函數表達式
{let a = 'secret';let f = function () {return a;};
}
另外,還有一個需要注意的地方。ES6 的塊級作用域必須有大括號,如果沒有大括號,JavaScript 引擎就認為不存在塊級作用域。
// 第一種寫法,報錯
if (true) let x = 1;// 第二種寫法,不報錯
if (true) {let x = 1;
}
上面代碼中,第一種寫法沒有大括號,所以不存在塊級作用域,而let
只能出現在當前作用域的頂層,所以報錯。第二種寫法有大括號,所以塊級作用域成立。
函數聲明也是如此,嚴格模式下,函數只能聲明在當前作用域的頂層。(我猜是因為如果沒有{},變量或者函數都存在當前作用域頂層的下一級非塊級作用域中(不知名作用域……),所以會報錯?)
// 不報錯
'use strict';
if (true) {function f() {}
}// 報錯
'use strict';
if (true)function f() {}
const
const
聲明一個只讀的常量。一旦聲明,常量的值就不能改變。
const PI = 3.1415;
PI // 3.1415PI = 3;
// TypeError: Assignment to constant variable.
上面代碼表明改變常量的值會報錯。
const
聲明的變量不得改變值,這意味著,const
一旦聲明變量,就必須立即初始化,不能留到以后賦值。
const foo;
// SyntaxError: Missing initializer in const declaration
上面代碼表示,對于const
來說,只聲明不賦值,就會報錯。
const
的作用域與let
命令相同:只在聲明所在的塊級作用域內有效。
if (true) {const MAX = 5;
}MAX // Uncaught ReferenceError: MAX is not defined
const
命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置后面使用。
if (true) {console.log(MAX); // ReferenceErrorconst MAX = 5;
}
上面代碼在常量MAX
聲明之前就調用,結果報錯。
const
聲明的常量,也與let
一樣不可重復聲明。
var message = "Hello!";
let age = 25;// 以下兩行都會報錯
const message = "Goodbye!";
const age = 30;
本質
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']; // 報錯
上面代碼中,常量a
是一個數組,這個數組本身是可寫的,但是如果將另一個數組賦值給a
,就會報錯。
如果真的想將對象凍結,應該使用Object.freeze
方法。
const foo = Object.freeze({});// 常規模式時,下面一行不起作用;
// 嚴格模式時,該行會報錯
foo.prop = 123;
上面代碼中,常量foo
指向一個凍結的對象,所以添加新屬性不起作用,嚴格模式時還會報錯。
除了將對象本身凍結,對象的屬性也應該凍結。下面是一個將對象徹底凍結的函數。(遞歸)
var constantize = (obj) => {Object.freeze(obj);Object.keys(obj).forEach( (key, i) => {if ( typeof obj[key] === 'object' ) {constantize( obj[key] );}});
};
ES6 聲明變量的六種方法
ES5 只有兩種聲明變量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,后面章節還會提到,另外兩種聲明變量的方法:import
命令和class
命令。所以,ES6 一共有 6 種聲明變量的方法。
頂層對象的屬性
頂層對象,在瀏覽器環境指的是window
對象,在 Node 指的是global
對象。ES5 之中,頂層對象的屬性與全局變量是等價的。
window.a = 1;
a // 1a = 2;
window.a // 2
這樣的設計帶來了幾個很大的問題,首先是沒法在編譯時就報出變量未聲明的錯誤,只有運行時才能知道(因為全局變量可能是頂層對象的屬性創造的,而屬性的創造是動態的);其次,程序員很容易不知不覺地就創建了全局變量(比如打字出錯);最后,頂層對象的屬性是到處可以讀寫的,這非常不利于模塊化編程。另一方面,window
對象有實體含義,指的是瀏覽器的窗口對象,頂層對象是一個有實體含義的對象,也是不合適的。
ES6 為了改變這一點,一方面規定,為了保持兼容性,var
命令和function
命令聲明的全局變量,依舊是頂層對象的屬性;另一方面規定,let
命令、const
命令、class
命令聲明的全局變量,不屬于頂層對象的屬性。也就是說,從 ES6 開始,全局變量將逐步與頂層對象的屬性脫鉤。
var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者采用通用方法,寫成 this.a
window.a // 1let b = 1;
window.b // undefined
上面代碼中,全局變量a
由var
命令聲明,所以它是頂層對象的屬性;全局變量b
由let
命令聲明,所以它不是頂層對象的屬性,返回undefined
。
globalThis 對象
JavaScript 語言存在一個頂層對象,它提供全局環境(即全局作用域),所有代碼都是在這個環境中運行。但是,頂層對象在各種實現里面是不統一的。
- 瀏覽器里面,頂層對象是
window
,但 Node 和 Web Worker 沒有window
。 - 瀏覽器和 Web Worker 里面,
self
也指向頂層對象,但是 Node 沒有self
。 - Node 里面,頂層對象是
global
,但其他環境都不支持。
同一段代碼為了能夠在各種環境,都能取到頂層對象,現在一般是使用this
變量,但是有局限性。
- 全局環境中,
this
會返回頂層對象。但是,Node 模塊和 ES6 模塊中,this
返回的是當前模塊。 - 函數里面的
this
,如果函數不是作為對象的方法運行,而是單純作為函數運行,this
會指向頂層對象。但是,嚴格模式下,這時this
會返回undefined
。 - 不管是嚴格模式,還是普通模式,
new Function('return this')()
,總是會返回全局對象。但是,如果瀏覽器用了 CSP(Content Security Policy,內容安全策略),那么eval
、new Function
這些方法都可能無法使用。
綜上所述,很難找到一種方法,可以在所有情況下,都取到頂層對象。下面是兩種勉強可以使用的方法。
// 方法一
(typeof window !== 'undefined'? window: (typeof process === 'object' &&typeof require === 'function' &&typeof global === 'object')? global: this);// 方法二
var getGlobal = function () {if (typeof self !== 'undefined') { return self; }if (typeof window !== 'undefined') { return window; }if (typeof global !== 'undefined') { return global; }throw new Error('unable to locate global object');
};
現在有一個提案,在語言標準的層面,引入globalThis
作為頂層對象。也就是說,任何環境下,globalThis
都是存在的,都可以從它拿到頂層對象,指向全局環境下的this
。
墊片庫global-this
模擬了這個提案,可以在所有環境拿到globalThis
。
變量的解構賦值
基本用法
ES6 允許按照一定模式,從數組和對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)。
let [a, b, c] = [1, 2, 3];
上面代碼表示,可以從數組中提取值,按照對應位置,對變量賦值。
本質上,這種寫法屬于“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3let [ , , third] = ["foo", "bar", "baz"];
third // "baz"let [x, , y] = [1, 2, 3];
x // 1
y // 3let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
另一種情況是不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的數組。這種情況下,解構依然可以成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
如果等號的右邊不是數組(或者嚴格地說,不是可遍歷的結構,參見《Iterator》一章),那么將會報錯。
// 報錯
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
上面的語句都會報錯,因為等號右邊的值,要么轉為對象以后不具備 Iterator 接口(前五個表達式),要么本身就不具備 Iterator 接口(最后一個表達式)。
對于 Set 結構,也可以使用數組的解構賦值。
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
事實上,只要某種數據結構具有 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
上面代碼中,fibs
是一個 Generator 函數(參見《Generator 函數》一章),原生具有 Iterator 接口。解構賦值會依次從這個接口獲取值。
默認值
解構賦值允許指定默認值。
let [foo = true] = [];
foo // truelet [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意,ES6 內部使用嚴格相等運算符(===
)判斷一個位置是否有值。所以,只有當一個數組成員嚴格等于undefined
,默認值才會生效。
let [x = 1] = [undefined];
x // 1let [x = 1] = [null];
x // null
上面代碼中,如果一個數組成員是null
,默認值就不會生效,因為null
不嚴格等于undefined
。
console.log(undefined === undefined) // true
console.log(null === null) // true
console.log(NaN === NaN) // false
console.log(null)//null
console.log(typeof null)//object
如果默認值是一個表達式,那么這個表達式是惰性求值的,即只有在用到的時候,才會求值。
function f() {console.log('aaa');
}let [x = f()] = [1];
上面代碼中,因為x
能取到值,所以函數f
根本不會執行。上面的代碼其實等價于下面的代碼。
let x;
if ([1][0] === undefined) {x = f();
} else {x = [1][0];
}
默認值可以引用解構賦值的其他變量,但該變量必須已經聲明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
上面最后一個表達式之所以會報錯,是因為x
用y
做默認值時,y
還沒有聲明。
對象的解構賦值
解構不僅可以用于數組,還可以用于對象。
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
。
如果解構失敗,變量的值等于undefined
。
let {foo} = {bar: 'baz'};
foo // undefined
對象的解構賦值,可以很方便地將現有對象的方法,賦值到某個變量。
// 例一
let { log, sin, cos } = Math;// 例二
const { log } = console;
log('hello') // hello
上面代碼的例一將Math
對象的對數、正弦、余弦三個方法,賦值到對應的變量上,使用起來就會方便很多。例二將console.log
賦值到log
變量。
如果變量名與屬性名不一致,必須寫成下面這樣。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
這實際上說明,對象的解構賦值是下面形式的簡寫(參見《對象的擴展》一章)。
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
也就是說,對象的解構賦值的內部機制,是先找到同名屬性,然后再賦給對應的變量。真正被賦值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
上面代碼中,foo
是匹配的模式,baz
才是變量。真正被賦值的是變量baz
,而不是模式foo
。
與數組一樣,解構也可以用于嵌套結構的對象。
let obj = {p: ['Hello',{ y: 'World' }]
};let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
注意,這時p
是模式,不是變量,因此不會被賦值。如果p
也要作為變量賦值,可以寫成下面這樣。
let obj = {p: ['Hello',{ y: 'World' }]
};let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]
下面是嵌套賦值的例子。
let obj = {};
let arr = [];({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });obj // {prop:123}
arr // [true]
如果解構模式是嵌套的對象,而且子對象所在的父屬性不存在,那么將會報錯。
// 報錯
let {foo: {bar}} = {baz: 'baz'};
上面代碼中,等號左邊對象的foo
屬性,對應一個子對象。該子對象的bar
屬性,解構時會報錯。原因很簡單,因為foo
這時等于undefined
,再取子屬性就會報錯。
注意,對象的解構賦值可以取到繼承的屬性。
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);const { foo } = obj1;
foo // "bar"
上面代碼中,對象obj1
的原型對象是obj2
。foo
屬性不是obj1
自身的屬性,而是繼承自obj2
的屬性,解構賦值可以取到這個屬性。
默認值
對象的解構也可以指定默認值。
var {x = 3} = {};
x // 3var {x, y = 5} = {x: 1};
x // 1
y // 5var {x: y = 3} = {};
y // 3var {x: y = 3} = {x: 5};
y // 5var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
默認值生效的條件是,對象的屬性值嚴格等于undefined
。
var {x = 3} = {x: undefined};
x // 3var {x = 3} = {x: null};
x // null
上面代碼中,屬性x
等于null
,因為null
與undefined
不嚴格相等,所以是個有效的賦值,導致默認值3
不會生效。
注意點
(1)如果要將一個已經聲明的變量用于解構賦值,必須非常小心。
// 錯誤的寫法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代碼的寫法會報錯,因為 JavaScript 引擎會將{x}
理解成一個代碼塊,從而發生語法錯誤(這里可以理解為代碼塊 = 代碼塊,所以會報錯)。只有不將大括號寫在行首,避免 JavaScript 將其解釋為代碼塊,才能解決這個問題。
// 正確的寫法
let x;
({x} = {x: 1});
上面代碼將整個解構賦值語句,放在一個圓括號里面,就可以正確執行。關于圓括號與解構賦值的關系,參見下文。
(2)解構賦值允許等號左邊的模式之中,不放置任何變量名。因此,可以寫出非常古怪的賦值表達式。
({} = [true, false]);
({} = 'abc');
({} = []);
上面的表達式雖然毫無意義,但是語法是合法的,可以執行。
(3)由于數組本質是特殊的對象,因此可以對數組進行對象屬性的解構。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
上面代碼對數組進行對象解構。數組arr
的0
鍵對應的值是1
,[arr.length - 1]
就是2
鍵,對應的值是3
。方括號這種寫法,屬于“屬性名表達式”(數組默認屬性名都是下標)(參見《對象的擴展》一章)。
字符串的解構賦值
字符串也可以解構賦值。這是因為此時,字符串被轉換成了一個類似數組的對象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
類似數組的對象都有一個length
屬性,因此還可以對這個屬性解構賦值。
let {length : len} = 'hello';
len // 5
數值和布爾值的解構賦值
解構賦值時,如果等號右邊是數值和布爾值,則會先轉為對象。
let {toString: s} = 123;
s === Number.prototype.toString // truelet {toString: s} = true;
s === Boolean.prototype.toString // true
上面代碼中,數值和布爾值的包裝對象都有toString
屬性,因此變量s
都能取到值。
解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉為對象。由于undefined
和null
無法轉為對象(可是null又稱為空對象,typeof null = object……),所以對它們進行解構賦值,都會報錯。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函數參數的解構賦值
函數的參數也可以使用解構賦值。
function add([x, y]){return x + y;
}add([1, 2]); // 3
上面代碼中,函數add
的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量x
和y
。對于函數內部的代碼來說,它們能感受到的參數就是x
和y
。
下面是另一個例子。
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函數參數的解構也可以使用默認值。
function move({x = 0, y = 0} = {}) {return [x, y];
}move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
上面代碼中,函數move
的參數是一個對象,通過對這個對象進行解構,得到變量x
和y
的值。如果解構失敗,x
和y
等于默認值。
注意,下面的寫法會得到不一樣的結果。
function move({x, y} = { x: 0, y: 0 }) {return [x, y];
}move({x: 3, y: 8}); // [3, 8] => move({x, y} = { x: 0, y: 0 } = {x: 3, y: 8})
move({x: 3}); // [3, undefined] => move({x, y} = { x: 0, y: 0 } = {x: 3, y: undefined})
move({}); // [undefined, undefined] => move({x, y} = { x: 0, y: 0 } = {x: undefined, y: undefined})
move(); // [0, 0] => move({x, y} = { x: 0, y: 0 })
上面代碼是為函數move
的參數指定默認值,而不是為變量x
和y
指定默認值,所以會得到與前一種寫法不同的結果。
undefined
就會觸發函數參數的默認值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
圓括號問題
解構賦值雖然很方便,但是解析起來并不容易。對于編譯器來說,一個式子到底是模式,還是表達式,沒有辦法從一開始就知道,必須解析到(或解析不到)等號才能知道。
由此帶來的問題是,如果模式中出現圓括號怎么處理。ES6 的規則是,只要有可能導致解構的歧義,就不得使用圓括號。
但是,這條規則實際上不那么容易辨別,處理起來相當麻煩。因此,建議只要有可能,就不要在模式中放置圓括號。
不能使用圓括號的情況
以下三種解構賦值不得使用圓括號。
(1)變量聲明語句
// 全部報錯
let [(a)] = [1];let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};let { o: ({ p: p }) } = { o: { p: 2 } };
上面 6 個語句都會報錯,因為它們都是變量聲明語句,模式不能使用圓括號。
(2)函數參數
函數參數也屬于變量聲明,因此不能帶有圓括號。
// 報錯
function f([(z)]) { return z; }
// 報錯
function f([z,(x)]) { return x; }
(3)賦值語句的模式
// 全部報錯,這樣就等于 語句 = 代碼塊
({ p: a }) = { p: 42 };
([a]) = [5];
上面代碼將整個模式放在圓括號之中,導致報錯。
// 報錯
[({ p: a }), { x: c }] = [{}, {}];
上面代碼將一部分模式放在圓括號之中,導致報錯。
可以使用圓括號的情況
可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號。
[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確
上面三行語句都可以正確執行,因為首先它們都是賦值語句,而不是聲明語句;其次它們的圓括號都不屬于模式的一部分。第一行語句中,模式是取數組的第一個成員,跟圓括號無關;第二行語句中,模式是p
,而不是d
;第三行語句與第一行語句的性質一致。
用途
變量的解構賦值用途很多。
(1)交換變量的值
let x = 1;
let y = 2;[x, y] = [y, x];
上面代碼交換變量x
和y
的值,這樣的寫法不僅簡潔,而且易讀,語義非常清晰。
(2)從函數返回多個值
函數只能返回一個值,如果要返回多個值,只能將它們放在數組或對象里返回。有了解構賦值,取出這些值就非常方便。
// 返回一個數組function example() {return [1, 2, 3];
}
let [a, b, c] = example();// 返回一個對象function example() {return {foo: 1,bar: 2};
}
let { foo, bar } = example();
(3)函數參數的定義
解構賦值可以方便地將一組參數與變量名對應起來。
// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
(4)提取 JSON 數據
解構賦值對提取 JSON 對象中的數據,尤其有用。
let jsonData = {id: 42,status: "OK",data: [867, 5309]
};let { id, status, data: number } = jsonData;console.log(id, status, number);
// 42, "OK", [867, 5309]
上面代碼可以快速提取 JSON 數據的值。
(5)函數參數的默認值
jQuery.ajax = function (url, {async = true,beforeSend = function () {},cache = true,complete = function () {},crossDomain = false,global = true,// ... more config
} = {}) { <=!!!!// ... do stuff
};
指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || 'default foo';
這樣的語句。
(6)遍歷 Map 結構
任何部署了 Iterator 接口的對象,都可以用for...of
循環遍歷。Map 結構原生支持 Iterator 接口,配合變量的解構賦值,獲取鍵名和鍵值就非常方便。
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');for (let [key, value] of map) {console.log(key + " is " + value);
}
// first is hello
// second is world
如果只想獲取鍵名,或者只想獲取鍵值,可以寫成下面這樣。
// 獲取鍵名
for (let [key] of map) {// ...
}// 獲取鍵值
for (let [,value] of map) {// ...
}
(7)輸入模塊的指定方法
加載模塊時,往往需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。
const { SourceMapConsumer, SourceNode } = require("source-map");
?
?
?