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