ES6~ES11 特性介紹之 ES6 篇

原本想稍微整理一下 ES 新特性,沒想到花了相當(dāng)多的時間拘荡,本文也巨長臼节,依然推薦使用 簡悅 生成目錄。
原文竟然由于過長無法發(fā)布珊皿,第一次知道簡書還有文章字?jǐn)?shù)限制⊥欤現(xiàn)在只能拆成兩篇發(fā)布。

本系列文章

ES6~ES11 特性介紹之 ES6 篇
ES6~ES11 特性介紹之 ES7~ES11 篇

一點歷史

JavaScript 與 Java

1994 年蟋定,著名的網(wǎng)景公司 Netscape 成立粉臊。同年 12 月份,網(wǎng)景發(fā)布了 Netscape Navigator 瀏覽器 1.0 版本驶兜,Navigator 一經(jīng)推出并占領(lǐng)了超過 90% 的市場份額扼仲。

同時由于當(dāng)時網(wǎng)費貴远寸、網(wǎng)速慢,網(wǎng)景很快意識到他們需要一個能夠運行在瀏覽器端的語言屠凶,將一些行為和邏輯放在瀏覽器端贱枣,從而減少與服務(wù)器端不必要的交互。而當(dāng)時網(wǎng)景正好與剛推出 Java 的 Sun 公司有合作惕艳,即在瀏覽器中支持 Java 小程序(Java Applet)。所以網(wǎng)景考慮過選擇 Java 作為瀏覽器端的語言郊楣,但由于 Java 對于瀏覽器來講還是「太重」,所以最終還是決定創(chuàng)造一個「足夠輕的腳本語言」贮懈,并且希望這門腳本語言的語法接近 Java搁嗓。

1995 年荐类,開發(fā)這個腳本語言的任務(wù)交給了當(dāng)時任職于網(wǎng)景的程序員 Brendan Eich。1995 年 5 月濒生,Brendan Eich 僅用了 10 天[1] 就完成了這門語言的第一版埋泵。 這門語言最初名為 Mocha幔欧,同年 9 月改名為 LiveScript。12 月份丽声,網(wǎng)景公司和 Sun 公司達成協(xié)議礁蔗,這門語言被允許叫做 JavaScript[2]12 月 4 日雁社,兩家聯(lián)合發(fā)布了 JavaScript浴井,并對外宣傳是 Java 的補充,是輕量級 Java霉撵,且專門用來操作網(wǎng)頁磺浙。

[1] 雖然這確實是一樁美談,但也不可否認(rèn)徒坡,過于簡單撕氧、短促的發(fā)明過程使得 JavaScript 這門語言留下了不少設(shè)計上的缺陷。如塊級作用域的缺失喇完、模塊化的缺失伦泥、nullundefined 的設(shè)計、== 的隱含轉(zhuǎn)換等锦溪,這些都導(dǎo)致 JavaScript 自誕生以來就一直被吐槽和批評不脯。實際直到今天,很多標(biāo)準(zhǔn)依然是在填補最初設(shè)計埋下的坑刻诊。
[2] 網(wǎng)景公司可以借助當(dāng)時 Java 的勢頭防楷,Sun 也可以借此拓展在瀏覽器端的影響力

從上面 JavaScript 的發(fā)明過程可以看出则涯,在語言設(shè)計層面上域帐,雖然在設(shè)計之初有著「接近或借鑒 Java」的初衷赘被,但 JavaScript 依然存在諸多的不同,JavaScript 與 Java 確實可以說是兩門完全不同的語言肖揣。但另一方面民假,在商業(yè)和歷史層面兩者又有著千絲萬縷的關(guān)聯(lián)

這感覺簡直就像是傳說中的「異父異母的親兄弟」龙优。

JavaScript 與 ECMAScript

1996 年 8 月羊异,與網(wǎng)景競爭的微軟按耐不住,開發(fā)了自己的 JavaScript 即 JScript彤断,并內(nèi)置于自家的 IE 3.0 瀏覽器野舶。

于是網(wǎng)景開始展示操作,同年 11 月宰衙,網(wǎng)景就將 JavaScript 提交給了國際標(biāo)準(zhǔn)化組織 ECMA(European Computer Manufacturers Association平道,歐洲計算機制造商協(xié)會),也就是想盡快的使自家 JavaScript 成為瀏覽器腳本語言的標(biāo)準(zhǔn)供炼,從而掌握瀏覽器腳本語言標(biāo)準(zhǔn)的主導(dǎo)權(quán)一屋。 這一標(biāo)準(zhǔn)的制定任務(wù)最終交給了 ECMA 的 39 號技術(shù)委員會(Technical Committee 39,簡稱 TC39)袋哼。

1997 年 7 月冀墨,ECMA 推出了標(biāo)準(zhǔn)文件 ECMA-262 第一版,作為瀏覽器腳本語言的第一版涛贯。該標(biāo)準(zhǔn)實際上基本就是按照 JavaScript 來制定的雏掠,但由于根據(jù)當(dāng)年網(wǎng)景與 Sun 公司的協(xié)議枕稀,JavaScript 這一名稱只能由網(wǎng)景使用射亏,同時也為體現(xiàn)該標(biāo)準(zhǔn)的制定者是 ECMA 而不是網(wǎng)景莽红,以便保持語言后續(xù)的開放性、中立性稀余,該語言最終被命名為 ECMAScript悦冀。

所以 ECMAScript 是 JavaScript 的標(biāo)準(zhǔn)與規(guī)范,JavaScript 是 ECMAScript 標(biāo)準(zhǔn)的實現(xiàn)滚躯。但在日常交流中雏门,兩者可以互換

ECMAScript 的歷史

1997 年 7 月掸掏,ECMAScript 1.0 發(fā)布茁影。次年即 1998 年 6 月,ECMAScript 2.0 發(fā)布丧凤。

1999 年 12 月募闲,ECMAScript 3.0 發(fā)布并受到廣泛支持,奠定了 JavaScript 的基本語法愿待。

2000 年啟動了 ECMAScript 4.0 的工作浩螺,2007 年 10 月 ECMAScript 4.0 的標(biāo)準(zhǔn)草案發(fā)布靴患,原先預(yù)計次年 8 月發(fā)布正式版。

但標(biāo)準(zhǔn)的制定者 TC39 內(nèi)部對 ECMAScript 4.0 產(chǎn)生了嚴(yán)重分歧和爭論要出。
Yahoo鸳君、Microsoft、Google 為首的大公司認(rèn)為 ECMAScript 4.0 改動過大患蹂,反對一次性發(fā)布如此巨大的變動或颊。而以 JavaScript 之父 Brendan Eich 為首的 Mozilla 公司則堅持當(dāng)前的標(biāo)準(zhǔn)。

最終結(jié)果是暫停 ECMAScript 4.0 的開發(fā)传于,但拿出本次討論的一些小修改[3]作為 ECMAScript 3.1 發(fā)布囱挑,還充滿內(nèi)涵的將該版本取名為 Harmony(和諧)。但會后不久 ECMAScript 3.1 被重命名為 ECMAScript 5[4]

[3] 主要是對原有特性的增強沼溜。
[4] 根據(jù)當(dāng)時的一些討論: Announcement: ES3.1 renamed to ES5 平挑。
可以看出改名的原因應(yīng)該是 ECMAScript 4.0 后期實現(xiàn)依然會很困難,如果不暫時躍過這個版本系草,后期發(fā)布可能會止步不前通熄。但同時又希望保留 ECMAScript 4.0 草案用以指導(dǎo)后面的標(biāo)準(zhǔn)制定。所以就暫時躍過 4.0 將 ECMAScript 3.1 直接作為 ECMAScript 5 正式發(fā)布悄但。
ECMAScript 4.0 草案中的很多標(biāo)準(zhǔn)實際上在 ECMAScript 6.0 中得以實現(xiàn)棠隐,當(dāng)然也有不少標(biāo)準(zhǔn)至今未實現(xiàn)石抡。

2009 年 12 月 ECMAScript 5 正式發(fā)布檐嚣。2011 年 6 月 ECMAScript 5.1 發(fā)布。

2015 年 6 月 ECMAScript 6 正式發(fā)布啰扛,由于是 2015 發(fā)布嚎京,所以 ECMAScript 6 也可以被稱為 ECMAScript 2015。由于種種原因隐解,從 ECMAScript 3.0ECMAScript 6 經(jīng)過了大約 15 年的時間鞍帝。但往后 ECMA 基本每一年都會發(fā)布一版新標(biāo)準(zhǔn),按照年份依次類推即可:

  • ECMAScript 7(ECMAScript 2016)
  • ECMAScript 8(ECMAScript 2017)
  • ECMAScript 9(ECMAScript 2018)
  • ECMAScript 10(ECMAScript 2019)
  • ECMAScript 11(ECMAScript 2020)

本文默認(rèn)讀者了解 JavaScript 的基本語法(即 ECMAScript 5.0)煞茫,然后梳理和介紹從 ES2015 開始的新特性帕涌。

特性列表

ES6(ES 2015) 特性

#01 let 與 const

let 帶來的能力主要有:

  • 塊級作用域
  • 約束「變量提升」
  • 不允許重復(fù)聲明
1.1 塊級作用域

ES6 以前,JavaScript 只有全局作用域和函數(shù)作用域续徽,這無異是一個極其糟糕的設(shè)計蚓曼。這會帶來很多不符合正常程序員直覺的現(xiàn)象:

  1. 內(nèi)層變量覆蓋外層變量
function funcA() {
  var a = 1;
  if (true) {
    var a = 2;
  }
  
  console.log(a); // 輸出為 2 
}
  1. 循環(huán)變量泄漏為全局變量
var s = "hello";
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // i 為全局變量,且輸出為 5

ES6 引入了 let 實現(xiàn)塊級作用域:

function funcA() {
  let a = 1;
  if (true) {
    let a = 2;
  }
  
  console.log(a); // 輸出為 1
}

let s = "hello";
for (let i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // i is not defined

關(guān)于塊級作用域钦扭,這里需要額外補充函數(shù)的場景纫版。

在 ES5 標(biāo)準(zhǔn)中,在塊級作用域內(nèi)聲明函數(shù)是非法[5]的客情。由于 ES6 標(biāo)準(zhǔn)引入了塊級作用域其弊,所以明確規(guī)定塊級函數(shù)聲明是合法的癞己,并且聲明的函數(shù)擁有塊級作用域,即在代碼塊之外是不可訪問的[6]梭伐。

[5] 但在一些瀏覽器的實際實現(xiàn)中痹雅,處于兼容舊代碼的原因,塊級函數(shù)聲明是被允許的糊识。
[6] 這是正文中的規(guī)定练慕,但是 ES6 的 附錄 B 中指出瀏覽器在實現(xiàn)時可以不按照這個規(guī)定,原因依然是處于舊代碼兼容性的考慮技掏。

1.2 約束「變量提升」

let 除了實現(xiàn)塊級作用域之外铃将,還有一個非常重要的能力,即約束「變量提升」哑梳。

所謂的「變量提升」指的是 JavaScript 的「變量可以在聲明之前使用」的現(xiàn)象:

// ES6 之前使用 var 聲明的變量會發(fā)生「變量提升」
// 腳本運行時劲阎,在未執(zhí)行 a  = 2 之前,a 就已經(jīng)存在并被初始化為 undefined
// 如同變量聲明被提升到了代碼頂部一樣
console.log(a); // 輸出 undefined
var a = 2;

「變量提升」的底層原因是 JavaScript 引擎在執(zhí)行代碼之前會對代碼進行編譯分析[7]鸠真,這個階段會將檢測到的變量和函數(shù)聲明添加到 JavaScript 引擎中名為 Lexical Environment 的內(nèi)存數(shù)據(jù)結(jié)構(gòu)中悯仙,并給予一個初始化值為 undefined。然后再進入代碼執(zhí)行階段吠卷。所以在代碼執(zhí)行之前锡垄,JS 引擎就已經(jīng)知曉聲明的變量和函數(shù)。

[7] 并不是編譯型語言的編譯祭隔,而是指對代碼進行詞法分析货岭、變量函數(shù)的檢測等工作的階段 。

這種 var 變量具有的「變量提升」現(xiàn)象無疑是詭異的疾渴,所以 let 聲明的變量將約束「變量提升」千贯。

之所以說約束,是因為 let 聲明的變量并沒有真正取消上述「變量提升」的過程搞坝,只是作出了一個關(guān)鍵變更搔谴。即將 let 變量添加到 Lexical Environment 后不再進行初始化為 undefined 的操作,JS 引擎只會在執(zhí)行到詞法聲明和賦值時才進行初始化桩撮。而在變量創(chuàng)建到真正初始化之間的時間跨度內(nèi)敦第,它們無法訪問或使用,ES6 將其稱之為暫時性死區(qū)( Temporal Dead Zone)店量。如下所示:

function funcA() {
   // 代碼運行前芜果,a 其實已經(jīng)被添加到 Lexical Environment 內(nèi)存數(shù)據(jù)結(jié)構(gòu)中
  // 但由于 a 未被初始化,所以 JS 引擎將禁止訪問它
  // 暫時性死區(qū) TDZ 開始
  a = 'abc';               // ReferenceError
  console.log(a);   // ReferenceError

  let a;                     // JS 引擎遇到詞法聲明垫桂,將 a 初始化為 undefined师幕。暫時性死區(qū) TDZ 結(jié)束
  console.log(a) ;  // 可以正常訪問 a
}
1.3 不允許重復(fù)聲明

在 ES6 之前,var 對同一個作用域內(nèi)的重復(fù)聲明沒有限制,甚至可以聲明與參數(shù)同名的變量霹粥,如:

function funcA() {
  var a = 1;
  var a = 2;
}
function funcB(args) {
  var args = 1; // 不會報錯
}

let 修復(fù)了這種不嚴(yán)謹(jǐn)?shù)脑O(shè)計:

function funcA() {
  let a = 1;
  let a = 2; // 報錯灭将,a 已經(jīng)被聲明過
}
function funcB(args) {
  let args = 1; // 報錯,args 已經(jīng)被聲明過
}

const 同樣擁有上述的三個特點后控,與 let 的不同的是它具有「只讀」的語義庙曙,一旦聲明初始化,不可再改變它的值浩淘。

但 const 的本質(zhì)是保障變量所指向的內(nèi)存地址里的數(shù)據(jù)不可改變捌朴。

對于數(shù)值、字符串张抄、布爾值等簡單類型砂蔽,對應(yīng)的值就存在變量所指向的地址里,所以使用 const 修飾可以保障其值不被修改署惯。但對于對象左驾、數(shù)組等復(fù)合類型,變量指向的內(nèi)存地址保存的是指向?qū)嶋H數(shù)據(jù)的指針极谊,此時 const 修飾對象诡右、數(shù)組并不能保障對象里的成員變量或數(shù)組內(nèi)的元素不被改變。

如果想使對象不可變轻猖,可以使用 Object.freeze

#02 變量解構(gòu)

2.1 數(shù)組解構(gòu)
2.1.1 基本語法

ES6 引入了一種從對象帆吻、數(shù)組等數(shù)據(jù)或表達式中提取值并賦值給變量的語法糖咙边。例如以前從數(shù)組中提取值并賦值給變量猜煮,需要編碼如下:

const arr = [1, 2, 3];
const a = arr[0];
const b = arr[1];
const c = arr[2];

解構(gòu)語法將簡化操作:

const [a, b, c] = [1, 2, 3];

es6 實現(xiàn)對 [1, 2, 3] 解構(gòu)并依次賦值變量 a、b样眠、c友瘤。數(shù)組的解構(gòu)賦值按照位置將值與變量對應(yīng)翠肘。

2.1.2 不完全解構(gòu)

數(shù)組解構(gòu)可以實現(xiàn)不完全解構(gòu)檐束,如下所示:

// 提取除第一個元素外的其他元素
const [, b, c] = [1, 2, 3];
// 提取除第二個元素外的其他元素
const [a, , c] = [1, 2, 3];
// 只提取最后一個元素
const [, , c] = [1, 2, 3];

如果解構(gòu)時對應(yīng)的位置沒有值,則變量將被賦值為 undefined

const [a, b, c, d] = [1, 2, 3];
console.log('undefined d:', d);  // d = undefined;
2.1.3 rest 操作符 ...

可以使用 rest 操作符 ... 來捕獲剩余項:

const [a, ...b] = [1, 2, 3];

console.log('rest a: ', a); // a = 1
console.log('rest b', b);   // b = [2, 3]
2.1.4 嵌套數(shù)組解構(gòu)

可以對復(fù)雜嵌套的數(shù)組進行解構(gòu):

const [a, [inner_1, inner_2], c] = [1, [2, 3], 4];
// a = 1; inner_1 = 1; inner_2 = 2; c = 3;
console.log(a, inner_1, inner_2, c);
2.1.5 支持默認(rèn)值

解構(gòu)時可以制定默認(rèn)值:

const [a, b, c, d = 4] = [1, 2, 3];
console.log(a, b, c, d); // d = 4

注意必須是對應(yīng)的值嚴(yán)格等于即=== undefined 時才會使用默認(rèn)值束倍,例如 null 并不會觸發(fā)默認(rèn)值:

// 不觸發(fā)默認(rèn)值 1被丧,null 被正常解構(gòu)并賦值給 x。
const [x = 1] = [null];
console.log('x:', x); // x = null;

默認(rèn)值可以使用表達式绪妹,且此時的表達式為惰性求值

const inner_func = () => 2;
const [y = inner_func()] = [undefined];

console.log('default_value y:', y);  // y = 2;
2.2 對象解構(gòu)
2.2.1 基本語法

解構(gòu)同樣可以作用于對象

const person = { name: "zhang", sex: "female", age: 18 };
const {name: name1, sex: sex1, age: age1} = person;
console.log(name1, sex1, age1);  // name1 = "zhang"; sex1 = "femail"; age1 = 18;

數(shù)組按照位置進行匹配賦值甥桂,而對象按照模式進行匹配。在上述代碼 {name: name1, sex: sex1, age: age1} 中邮旷,符號 : 左邊為匹配的模式黄选,符號 : 右邊為被賦值的變量名稱。即 const {name: name1, sex: sex1, age: age1} = person 的語義為取出對象 person 中的 name 字段賦值給 name1 變量...

當(dāng)然可以將變量名起名為 name

const {name: name, sex: sex, age: age} = person;

此時可以對上述寫法進一步簡化:

// name = person.name;
// sex = person.sex;
// age = person.age;
const {name, sex, age} = person;
2.2.2 嵌套解構(gòu)賦值

復(fù)雜的嵌套對象依然可以使用解構(gòu)語法進行解構(gòu)賦值:

const person = { name: "zhang", sex: "female", age: 18, addr: {city: "shenzhen", street: "street1"}};
const {addr: {city, street: default_street}} = person;

console.log('city:', city, 'street:', default_street);  // city = "shenzhen"; street = street1;

再結(jié)合上述的嵌套數(shù)組,對于更為復(fù)雜的嵌套數(shù)組和對象的結(jié)構(gòu)也可以進行解構(gòu)賦值:

const person = {
  name: "zhang",
  sex: "female",
  age: 18,
  more_infos: [
    {
       company: "xxx_company"
    },
    {
      addr: {city: "shenzhen", street: "street1"}
    }
  ]
};

const {more_infos: [{ company }, { addr: {city: city1}}]} = person;
console.log('company:', company, 'city1:', city1); // company = "xxx_company"; city1="shenzhen";
2.2.3 支持默認(rèn)值

對象解構(gòu)同樣支持默認(rèn)值办陷,且對象解構(gòu)默認(rèn)值的生效條件同樣是:=== undefined貌夕,所以 null 會被經(jīng)常解構(gòu)賦值而不會應(yīng)用默認(rèn)值:

const person = { name: "zhang", sex: "female", age: 18 };
const {interest1="watch moive"} = person;
console.log('interest1', interest1);  // interest1 = "watch moive"

const {interest2="watch moive"} = {interest2: null};
console.log('interest2', interest2);  // interest2 = null
2.2.4 對已聲明變量進行解構(gòu)賦值

上文的所有示例代碼在進行解構(gòu)賦值都是配合 letconst 進行的民镜,即聲明解構(gòu)賦值 同時進行啡专。如果直接對已經(jīng)聲明的變量進行賦值將會報錯:

const person = { name: "zhang", sex: "female", age: 18 };
let age;

{ age } = person;  // 報錯

上述代碼是非法的,因為 JS 引擎在缺乏let制圈、const们童、var 關(guān)鍵詞時,將會把 {age} 理解為代碼塊從而導(dǎo)致語法錯誤鲸鹦。

解決的方法是加上圓括號慧库,如下所示:

const person = { name: "zhang", sex: "female", age: 18 };
let age;
({age} = person);  // age=18;

給某個已存在的變量賦值可能就經(jīng)常遇到:

const me = {
    age: undefined
};

// 從 person 解構(gòu)并給 me.age 賦值
({age: me.age} = person);
console.log('me', me); // me = {age: 18};
2.3 其他類型解構(gòu)
2.3.1 字符串解構(gòu)

可以對字符串解構(gòu)(字符串將被當(dāng)成數(shù)組):

const [a, b, c] = 'lcy';
console.log(a, b, c); // a = 'l'; b = 'c'; c = 'y';

捕獲字符串屬性:

const {length : len} = 'lcy';
console.log('lcy\'s length: ', len);  // len = 3; 
2.3.2 數(shù)值和布爾值解構(gòu)

對數(shù)值和布爾值進行解構(gòu)時,它們將會先被轉(zhuǎn)為對象馋嗜,然后再應(yīng)用解構(gòu)語法:

const {toString: number_tostring} = 123;
// number_tostring 為 Number 的 toString 方法
console.log(number_tostring === Number.prototype.toString);  // true

const {toString: bool_tostring} = true;
// bool_tostring 為 Boolean 的 toString 方法
console.log(bool_tostring === Boolean.prototype.toString);  // true
2.4 函數(shù)參數(shù)解構(gòu)

可以對函數(shù)的參數(shù)進行解構(gòu):

const inner_func = ([x, y, z]) => {
  return x + y + z;
}

const sum = inner_func([1, 2, 3]);
console.log('sum:', sum);  // sum = 6;

函數(shù)的參數(shù)解構(gòu)可以給我們帶來更好的編程體驗和風(fēng)格完沪。

在平時編程時,函數(shù)的參數(shù)過多不是一個好的設(shè)計嵌戈,例如:

const inner_func = (isOpen, isHook, names, ops, ...) => {
}

// 調(diào)用
inner_func(true, false, [1, 2], [1], ...);

上述代碼中 inner_func 的調(diào)用具有一定的心智負(fù)擔(dān)覆积,參數(shù)較多較混亂,調(diào)用者較容易傳錯參數(shù)熟呛。

所以我們經(jīng)常使用一個對象來承接過多的參數(shù)宽档,如下所示:

const inner_func = (options) => {
  const isOpen = options.isOpen || 'false'; // 使用 || 賦予默認(rèn)值 
  const isHook = options.isHook || 'true'; 
  const names = options.names || [];
  const ops = options.ops || [];
  // ....
}

上述的代碼使函數(shù)調(diào)用更為清晰和聚焦,但也存在一定的缺點庵朝,即 inner_func 函數(shù)的實現(xiàn)不夠優(yōu)雅吗冤,函數(shù)實現(xiàn)時無法直觀的了解 options 內(nèi)對象。

此處可以用解構(gòu)語法九府,優(yōu)化上述代碼:

const inner_func = ({isOpen=false, isHook=true, names=[], ops=[]}) => {
  // 馬上使用 isOpen椎瘟、isHook、names侄旬、ops 等  
}

還可以利用函數(shù)作為默認(rèn)值肺蔚,來實現(xiàn)一些更為靈活的能力,例如實現(xiàn)函數(shù)的必填參數(shù)

function requiredParam(param) {
  throw new Error(`Error: Required parameter ${param} is missing`);
}
    
const inner_func4 = ({id=requiredParam('id'), isOpen=false, isHook=true, names=[], ops=[]}) => {
  // ...
}
    
inner_func4({});  // 如果不指定 id儡羔,inner_func4 將會報錯
2.5 解構(gòu)語法的用途舉例

解構(gòu)語法可以靈活運用宣羊,除了上文中已提及的代碼,這里再舉一些常用的用途汰蜘。

解構(gòu) Map

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

// 配合 for...of 進行解構(gòu)賦值
for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// 僅獲取鍵名
for (let [key] of map) {
  // ...
}
// 僅獲取鍵值
for (let [,value] of map) {
  // ...
}

解構(gòu)模塊

const { UserUtils, AdminUtils } = require("utils");

解構(gòu)函數(shù)返回值

function inner_func() {
  return [1, 2, 3];
}

const [a, b, c] = inner_func();

#03 類 Class

3.1 ES6 之前的對象

JavaScript 在設(shè)計之初就引入了「面向?qū)ο?/strong>」的設(shè)計理念仇冯。但由于設(shè)計過程十分倉促,所以比起 Java/C++ 等語言族操,JavaScript 「面向?qū)ο?/strong>」的實現(xiàn)并不是很嚴(yán)謹(jǐn)苛坚。

在 ES6 之前,JavaScript 的對象體系并非基于「」,而是基于「函數(shù)」與「原型鏈」泼舱,如下所示:

// 為了與普通函數(shù)做區(qū)分
// 在編程規(guī)約上姐赡,函數(shù)名稱首字母打斜
const Dog = function(name, sex, age) {
  // this 關(guān)鍵字表示生成的對象實例本身
  // 但必須要配合 new 關(guān)鍵字才能真正指向?qū)ο髮嵗?  this.name = name;
  this.sex = sex;
  this.age = age;
}

const dog1 = Dog();       // dog1 = undefined
const dog2 = new Dog();   // dog2 = { name: 'mimi', sex: 'male', age: 1 }

如上述代碼所示,Dog 函數(shù) 可以被視為普通函數(shù)進行調(diào)用即 const dog1 = Dog();柠掂,這時候 dog1undefined项滑,因為 Dog 完全就是一個普通函數(shù),在這個函數(shù)內(nèi)沒有 return 任何值涯贞,并且其中的 this 指向當(dāng)前的運行時上下文(全局對象) 枪狂。

如果 Dog 函數(shù) 想要成為一個能夠創(chuàng)建對象實例的「構(gòu)造函數(shù)」,則必須要配合 new 關(guān)鍵字即 const dog2 = new Dog();宋渔,其中new 關(guān)鍵字主要完成了如下工作:

  • 創(chuàng)建一個空對象州疾,作為即將返回的對象實例
  • 將這個空對象的原型指向「構(gòu)造函數(shù)」的 prototype 屬性
  • 將這個空對象賦值給函數(shù)內(nèi)部的 this 關(guān)鍵字
  • 執(zhí)行「構(gòu)造函數(shù)」內(nèi)的代碼,例如 this.name = name皇拣,將 value 賦值給當(dāng)前對象屬性
  • 如果「構(gòu)造函數(shù)」內(nèi)寫了 return 語句且返回的是一個對象類型严蓖,則 new 將返回該對象。否則 new 將默認(rèn)返回 this 即當(dāng)前對象

上述步驟的第二步涉及到的 「指向構(gòu)造函數(shù)的 prototype 屬性」就是 JavaScript 實現(xiàn)對象體系的關(guān)鍵:

  1. 構(gòu)造函數(shù)具有 prototype 屬性氧急,指向構(gòu)造函數(shù)對應(yīng)的原型(可理解為其他語言中的「類」)
  2. 所有通過構(gòu)造函數(shù)創(chuàng)建的實例對象颗胡,會存在一個默認(rèn)屬性指向上述的原型(瀏覽器在具體實現(xiàn)時這個屬性通常命名為 proto
  3. 上述的原型本身又會有 __proto__ 屬性,指向原型的「原型」(上述 Dog 函數(shù)的原型的原型就是 頂層原型 Object)

這樣通過 __proto__ 就會形成一條「原型鏈」吩坝,通過「原型鏈」就可以找到對象實例對應(yīng)「類」以及「父類」和「父類的父類」[8]...

[8] 嚴(yán)格來講類和原型還是有所不同的毒姨。原型也被稱為「原型對象」,所以原型本質(zhì)上依然是一個對象钉寝。這里說「類」僅僅是為了理解方便弧呐。

由上可知,如果想要實現(xiàn)繼承嵌纲,需要將自己掛在父類的原型鏈之下:

// 第一步:繼承構(gòu)造函數(shù)
const Husky = function(name, sex, age, isStupid) {
  Dog.call(this, name, sex, age);
  this.isStupid = true;
}
// 第二步:子類的原型指向父類的原型
Husky.prototype = Object.create(Dog.prototype);
Husky.prototype.constructor = Husky;  // 修正原型 constructor 的指向

// 測試子類
const husky1 = new Husky('husky', 'male', 2);
// Dog 添加 sayHi 函數(shù)
Dog.prototype.sayHi = function() { console.log('wangwang'); }
console.log('husky1', husky1);
husky1.sayHi();  // 輸出 wangwang俘枫,Dog 原型擁有 sayHi() 方法
console.log('husky1 toString', husky1.toString());  // Object 原型擁有 toString() 方法

在訪問上述代碼中的 Husky 實例 husky1 的屬性和方法時,會首先查詢 husky1 本身的屬性和方法逮走,如果找不到則會查找其原型(Dog)鸠蚪,如果還未找到,則繼續(xù)查找原型的原型(Object)言沐,即按照原型鏈依次查找邓嘹。

除了上述例子外,ES6 之前的繼承還可以有其他寫法险胰,這里不再擴展。

3.2 ES6 的 class
3.2.1 基本語法

由上不難看出 ES6 之前的「面向?qū)ο蟆箤懛◤?fù)雜矿筝,不夠直觀降允。于是 ES6 引入了類 Class 的概念泪喊,使得 JavaScript 終于可以像其他語言那樣聲明類了:

class Dog {
  /**
    * 構(gòu)造函數(shù)夺英,相當(dāng)于之前的 function Dog()
    * 如果不顯式定義肩祥,則會默認(rèn)添加一個空的 constructor() {}
    **/
  constructor(name, sex, age) {
    this.name = name;
    this.sex = sex;
    this.age = age;
  }
  
  /** 自定義成員函數(shù) */
  sayHi() {
    console.log("wangwang");
  }
}

實際上 ES6 的 class 可以視作一種語法糖,只是在語法層面提供了將「構(gòu)造函數(shù)」和「自定義原型屬性」寫到一起的封裝寫法寇荧。底層的對象體系依然是基于函數(shù)和原型鏈。如上例代碼中將 sayHi() 定義在 class 塊內(nèi),相當(dāng)于執(zhí)行了 Dog.prototype.sayHi = function() { console.log('wangwang'); } 怯伊。class 的引入也使得對象的使用更為嚴(yán)謹(jǐn),傳統(tǒng)定義的 Dog 函數(shù)判沟,可以不加 new 從而被視為普通函數(shù)耿芹,而 class 定義的 Dog 必須通過 new 進行調(diào)用。同時注意 class 關(guān)鍵字和 let挪哄、const 等一樣吧秕,約束了變量提升。

classfunction 一樣有表達式的寫法:

// DogInner 對外不可見
const Dog = class DogInner { /* ... */ };
const Dog = class { /* ... */ };

// 甚至還可以有立即執(zhí)行的 class
const dog1 = new class {
  constructor(name) {
    this.name = name;
  }
}('旺財');
3.2.2 靜態(tài)方法

類是對象的模板迹炼,而對象是類的實例砸彬。由類生成對象時,對象應(yīng)當(dāng)具有類中定義的方法和屬性斯入。但實際上砂碉,并不是類中的所有方法、屬性都應(yīng)該賦予對象刻两。

例如現(xiàn)有一個類 Person绽淘,而 小紅小黃Person 的兩個實例化對象沪铭。Person 類中有屬性 planet = earth杀怠,那么包括小紅小黃在內(nèi)的任何 Person 實例都應(yīng)該擁有同樣的屬性值 planet = earth赔退。

每個實例都擁有的共同屬性比起被每個實例繼承,不如將其視為 Person 這個類本身的屬性证舟,即不是 小紅.planet小黃.planet 而是 Person.planet 硕旗。這樣的屬性稱之為靜態(tài)屬性,類似的方法稱為靜態(tài)方法女责。但遺憾的是 ES6 還未支持靜態(tài)屬性漆枚,但實現(xiàn)了靜態(tài)方法。靜態(tài)方法使用 static 關(guān)鍵字修飾:

class Dog {
  // ....

  static showPlanet() {
    console.log("汪星球");
  }
}

// 類名.方法名 的形式進行調(diào)用
Dog.showPlanet();
3.2.3 class 繼承

在上文的 3.1 節(jié)中介紹了 ES6 之前需要手動修改原型鏈才能實現(xiàn)繼承抵知,ES6 引入 extends 來使得繼承更為清晰:

class Husky extends Dog {
  // 如果沒有顯式定義 constructor
  // 則會自動添加一個默認(rèn)構(gòu)造函數(shù):
  // constructor(...args) {
  //   super(...args);
  // }
  constructor(name, sex, age, isStupid) {
    super(name, sex, age); // 調(diào)用父類的 constructor(name, sex, age);
    this.isStupid = isStupid;
  }
}

關(guān)于 super

  • 作為函數(shù):即 super(...)墙基,super 函數(shù)代表父類的構(gòu)造函數(shù)软族,只能用在子類構(gòu)造函數(shù)中。例如:
class Husky extends Dog {
  constructor(name, sex, age, isStupid) {
    super(name, sex, age); // 相當(dāng)于調(diào)用 Dog.prototype.constructor.call(this, name, sex, age);
    this.isStupid = isStupid;
  }

  sayHi() {
    super();  // 錯誤残制,不可在普通方法內(nèi)調(diào)用 super();
  }
}
  • 作為對象:即 super.xxx初茶,super 對象在普通方法中指向父類的原型對象如 Dog.prototype,在靜態(tài)方法中温峭,指向父類堕伪。例如:
class Husky extends Dog {
  constructor(name, sex, age, isStupid) {
    super(name, sex, age);
    this.isStupid = isStupid;
  }

  sayHi() {
    // 相當(dāng)于 Dog.prototype.sayHi.call(this)。在普通方法內(nèi)禁炒,super 表示父類的原型對象悠瞬。
    super.sayHi(); 
  }

  static showPlanet() {
    // super 表示父類望迎,此處調(diào)用了父類的靜態(tài)方法
    // 注意類的靜態(tài)方法掛在「類」下趴乡,即 Dog.xxx
    // 類的普通方法和構(gòu)造函數(shù)掛在「類的原型對象」下蒿涎,即 Dog.prototype.xxx
    super.showPlanet(); 
  }
}

#04 模塊 Module

Brendan Eich 可能一開始只是抱著開發(fā)玩具語言的心態(tài)創(chuàng)造了 JavaScript胖齐,所以他基本想象不到 JavaScript 后續(xù)會成為前端領(lǐng)域的標(biāo)準(zhǔn)語言,他更沒法預(yù)料到箫锤,隨著 JavaScript 的發(fā)展和推廣氛堕,這門語言開始被用來編寫越來越復(fù)雜和龐大的系統(tǒng)。如果他一開始能夠意識到這一點帮寻,我相信他一定會在最開始就提供模塊的能力蝉稳,因為復(fù)雜系統(tǒng)的開發(fā)離不開模塊化的支持。

正因為 JavaScript 在設(shè)計之初就缺失了模塊體系饿这,所以導(dǎo)致開發(fā)人員在后續(xù)十多年都在不斷探索和自行推動模塊化規(guī)范。從「函數(shù)封裝」到「對象封裝」再到「立即執(zhí)行函數(shù) IIFE」,以及后來的 CommonJS 規(guī)范卧蜓、AMD 規(guī)范盛霎、CMD 規(guī)范剂邮。雖然其中很多都已經(jīng)成為歷史,但這些五花八門的模塊化方案和規(guī)范蘊含了開發(fā)人員的努力和智慧。關(guān)于 JavaScript 模塊化的演化過程和歷史可以參見 JavaScript 模塊化的前世今生屑柔。

雖然在過去長期的工程實踐中,「民間」已經(jīng)自行推出了不少模塊化規(guī)范措译,有些還得到充分的實現(xiàn)和發(fā)展掠械。但我們最終期待的還是由官方制定的統(tǒng)一的模塊化方案肚菠,這一期待終于在 ES6 中得以實現(xiàn)烙荷。

ES6 的模塊化規(guī)范也被稱為 ESM,下文就以名稱表述 ES6 的模塊化規(guī)范耙蔑。

4.1 模塊導(dǎo)出
4.1.1 基本語法

ESM 中的一個模塊就是一個文件钱豁,并通過 export 關(guān)鍵字實現(xiàn)模塊導(dǎo)出:

/* utils.js */
export const _name = 'utils';
export const _desc = 'utils for dog and cat';
export function HandleDog() {/*...*/}; 
export function HandleCat() {/*...*/}; 

上述代碼對外導(dǎo)出了 _name幌蚊、_desc 變量以及 HandleDog搓茬、HandleCat 函數(shù),export 還有另一種寫法:

const _name = 'utils';
const _desc = 'utils for dog and cat';
const HandleDog = function() {/* ... */};
const HandleCat = function() {/* ... */};

export { _name, _desc, HandleDog, HandleCat };
// 導(dǎo)出非 default 變量時必須加括號,以下語法是錯誤的
// 錯誤用法:export _name;
// 錯誤用法:export 123; 
4.1.2 導(dǎo)出變量重命名

可以使用 as 關(guān)鍵字對導(dǎo)出的變量重命名:

// as 重命名時膊夹,同一個變量如 _name 可以賦予兩個不同名字
export {
  _name as name,
  _name as another_name,
  _desc as desc,
  HandleDog as handleDog,
  HandleCat as handleCat,
};
4.1.3 默認(rèn)導(dǎo)出

上文通過 export 導(dǎo)出的變量都是有名字的,那么在使用下文即將介紹的 import 時就需要指定名稱,如:

import { name } from '模塊';

但有時候模塊的使用者希望模塊能有一個「默認(rèn)輸出」,即:

import xxx from '模塊';
xxx();

其中 xxx 不是模塊內(nèi)部的某個變量名,而是在 import 未指定變量名時返回的默認(rèn)輸出愿吹,這個默認(rèn)輸出被起名為 xxx撵颊。如果這個默認(rèn)輸出是個函數(shù),則可以像上述代碼那樣直接進行調(diào)用 xxx()

這種默認(rèn)輸出可以在一些場合簡化模塊的使用帆喇,而在模塊內(nèi)我們使用 export default 命令指定本模塊的默認(rèn)輸出。

const _name = 'utils';
const _desc = 'utils for dog and cat';
const HandleDog = function() {/* ... */};
const HandleCat = function() {/* ... */};
const HandleDefault = function() {/* ... */};

// export default 無需添加大括號 {}
// 相當(dāng)于將 HandleDefault 賦值給 default 變量
export default HandleDefault;

export default 進行默認(rèn)導(dǎo)出無需添加大括號 {},相當(dāng)于將導(dǎo)出值如上面的 HandleDefault 函數(shù) 賦值給 default 變量呈昔。同時要注意到一個模塊只有一個「默認(rèn)輸出」郭宝,所以 export default 只能使用一次衔统。

4.2 模塊導(dǎo)入
4.2.1 按名稱導(dǎo)入

ESM 使用 import 命令實現(xiàn)導(dǎo)入加載模塊:

// 獲取 utils 模塊導(dǎo)出的變量
import { name, another_name, handleDog, handleCat } from './utils.js';

// 如上所示险掀,使用 import 導(dǎo)入時埠啃,括號內(nèi)的變量名需要和 utils 模塊導(dǎo)出的變量名相同
// 當(dāng)然也可以在導(dǎo)入的同時使用 as 起一個新名稱
import { name as old_name, another_name as new_name } from './utils.js';
4.2.2 整體導(dǎo)入

上面的導(dǎo)入語法是將需要的變量依次導(dǎo)入知押,如果想將模塊導(dǎo)出的所有變量作為一個整體一次性導(dǎo)入到一個對象,可使用 * 實現(xiàn)整體加載:

// .utils.js 模塊中通過 export 導(dǎo)出的所有變量將都掛到 Utils 之下
import * as Utils from './utils.js';
console.log(Utils.name);
console.log(Utils.another_name);
Utils.handleDog();
// ...
4.2.3 默認(rèn)導(dǎo)入

上文已經(jīng)提及「默認(rèn)導(dǎo)出」無需使用 {},而在導(dǎo)入「默認(rèn)導(dǎo)出」時也不需要使用 {}痊银。

因為一個模塊只有一個「默認(rèn)輸出」,且對應(yīng)的變量名為 default,所以在導(dǎo)入時無需指定特定名稱矛绘,還可以直接為其自定義一個任意名稱:

// 模塊內(nèi)導(dǎo)出的 HandleDefault 已經(jīng)被賦值為默認(rèn) default,所以導(dǎo)出的是 default 變量名
// 獲取模塊的默認(rèn) default 時無需通過模塊內(nèi)的定義特定名稱獲取
// 直接給這個默認(rèn) default 自定義一個任意名稱即可,例如 HandleOther
import HandleOther from './utils.js';
4.2.4 import 的一些特性
  1. import 導(dǎo)入的變量是只讀的,不可直接覆蓋導(dǎo)出的變量涌穆,例如:
import { name, another_name, handleDog, handleCat } from './utils.js';

// 模塊導(dǎo)出的變量為 const 變量
name = 'new utils';  // 錯誤,name 是只讀的
handleCat.new_name = 'new name';  // 如果導(dǎo)出的是對象類型,可以在該類型上添加屬性。但不建議這么做
  1. import 具有提升效果:
handleDog(); // 合法,因為下面的 import 語句在編譯期(語句分析階段)就被執(zhí)行了寿桨,早于 `handleDog 函數(shù)` 的調(diào)用此衅。

import { name, another_name, handleDog, handleCat } from './utils.js';
  1. import 編譯期(語句分析階段)執(zhí)行,所以是「靜態(tài)」的亭螟,無法使用表達式墨微、變量等運行時才可確定的值:
import { 'n' + 'ame' } from './utils.js';  // 錯誤

const module_name = './utils.js';
import { name } from module_name;  // 錯誤
  1. 重復(fù)加載氓奈,模塊語句只會被執(zhí)行一次:
// 加載三次纱兑,./utils.js 模塊內(nèi)的語句只會被執(zhí)行一次
import { name } from './utils.js';
import { handleDog } from './utils.js';
import { handleCat } from './utils.js';

// 上面語句效果與下面語句是等價的
import { name, handleDog, handleCat } from './utils.js';
  1. 導(dǎo)入的是原始值的引用
    import 導(dǎo)入的值是對模塊內(nèi)原始值的引用,這意味著不同文件導(dǎo)入和值和原始值相互影響:
import { name, changeName } from 'utils';

console.log(name);  // utils
changeName('changed');

 // name 為指向模塊內(nèi)部原始值的引用,原始值被修改允蚣,name 變量也會跟著變化
console.log(name);  // changed
4.3 導(dǎo)入導(dǎo)出混合寫法

在一些場景中浪秘,我們可能需要在某個文件里導(dǎo)入某些模塊,同時將這些模塊再導(dǎo)出。這個過程中可能都不會使用導(dǎo)入的模塊,只是起到模塊轉(zhuǎn)發(fā)的作用裆操。此時可以使用上文介紹的 exportimport 語法混合在一起的特殊語法:

// 「導(dǎo)入」又立馬「導(dǎo)出」掌敬,實際是「轉(zhuǎn)發(fā)」
export { name } from './utils.js';
// 上面語句等同于
import { name } from './utils.js';
export { name };

整體轉(zhuǎn)發(fā):

// 整體「轉(zhuǎn)發(fā)」
export * from './utils.js';

默認(rèn)轉(zhuǎn)發(fā):

// 直接轉(zhuǎn)發(fā)原有默認(rèn)輸出
export { default } from './utils.js';

// 轉(zhuǎn)發(fā)時修改默認(rèn)輸出,將 name 作為默認(rèn)輸出
export { name as default } from './utils.js';

// 等同于
import { name } from './utils.js';
export default name;

#05 函數(shù)擴展

ES6 對函數(shù)進行了語法、功能等擴展阶牍,包括參數(shù)默認(rèn)值、箭頭函數(shù)聘惦、尾調(diào)用優(yōu)化等砖第。

5.1 支持參數(shù)默認(rèn)值

ES6 以前复哆,不能為函數(shù)的參數(shù)設(shè)置默認(rèn)值,ES6 實現(xiàn)了對此的支持:

function getPoint(x = 0, y = 0) {
  console.log(x, y);
}

getPoint(1, 2);

undefined 才可觸發(fā)默認(rèn)值:

function getPoint(x = 0, y = 0, z = 0) {
  console.log(x, y, z);
}

getPoint(, , 3);  // 錯誤敲街,不可省略前面兩個參數(shù)
getPoint(undefined, undefined, 3); // 正確绒北,使用 undefined 觸發(fā)默認(rèn)值
getPoint(1, 2);  // 默認(rèn)值可實現(xiàn)參數(shù)的省略

由上例代碼中的 getPoint(1, 2) 可知,參數(shù)默認(rèn)值可以實現(xiàn)參數(shù)的省略,簡化函數(shù)的調(diào)用。但這通常是建立在「設(shè)置默認(rèn)值的是尾部參數(shù)」這個前提,如果是為非尾部參數(shù)設(shè)置默認(rèn)值,調(diào)用時參數(shù)不能被省略忘巧,需要使用 undefined 填充觸發(fā)默認(rèn)值恒界。

參數(shù)默認(rèn)值稍微復(fù)雜點的場景是和解構(gòu)語法一起出現(xiàn):

function getPoint({x = 0, y = 0}) {
  console.log(x, y);
}

getPoint({x: 1, y: 2});

引入?yún)?shù)默認(rèn)值之后,有幾點改變需要特別注意:

  1. 函數(shù)的 length 屬性砚嘴。

函數(shù)的 length 屬性本意為:「該函數(shù)期待的參數(shù)個數(shù)」十酣。在參數(shù)默認(rèn)值被引入之前涩拙,可以通過 length 獲得該函數(shù)定義的參數(shù)個數(shù),但是引入默認(rèn)值之后耸采,length 表達的是「第一個默認(rèn)值參數(shù)之前的普通參數(shù)個數(shù)(length 也不統(tǒng)計 rest 類型參數(shù))」兴泥。如下所示:

const funcA = function(x, y) {
};
console.log(funcA.length);  // 函數(shù)期待 x,y 兩個參數(shù),則長度輸出 2 

const funcB = function(x, y = 1) {
};
console.log(funcB.length);  // y 提供了默認(rèn)值虾宇,length 表示 y 之前有幾個非默認(rèn)值參數(shù)搓彻,長度輸出為 1

const funcC = function(x = 1, y) {
};
console.log(funcC.length);  // 輸出為 0 
  1. 參數(shù)作用域
    設(shè)置了參數(shù)默認(rèn)值后,將導(dǎo)致一個與以前不同的行為:參數(shù)在被初始化時將形成一個獨立作用域嘱朽,初始化完成后作用域消解好唯。看如下代碼并可理解:
let x = 1;

// y 設(shè)置了默認(rèn)值
// 則 funcA 被調(diào)用時燥翅,其中的參數(shù) x, y 將形成一個獨立的作用域
// 所以 y = x 中的 x 是第一個參數(shù) x,而不是上面定義的 let x= 1;
function funcA(x, y = x) {
  console.log(y);
}

// 在參數(shù)作用域中蜕提,y = x 語句的 x 是參數(shù) x 的值森书,即 2 而不是上面的 let x = 1,所以最終輸出為 2
funcA(2);  
5.2 箭頭函數(shù)
5.2.1 箭頭函數(shù)基本語法

ES6 引入了「箭頭函數(shù)」的語法來簡化函數(shù)的定義谎势,基本語法如下所示:

// 1. 不傳入?yún)?shù)
const funcA = () => console.log('funcA');
// 等價于
const funcA = function() {
  console.log('funcA');
} 

// 2. 傳入?yún)?shù)
const funcB = (x, y) => x + y;
// 等價于
const funcB = function(x, y) {
  return x + y;
} 

// 3. 單個參數(shù)的簡化
const funcC = (x) => x;
// 對于單個參數(shù)凛膏,可以去掉 (),簡化為
const funcC = x => x;
// 等價于
const funcC = function(x) {
  return x;
}

// 4. 上述代碼函數(shù)體只有單條語句脏榆,如果有多條猖毫,需要使用 {}
const funcD = (x, y) => { console.log(x, y); return x + y; }
// 等價于
const funcD = function(x, y) {
  console.log(x, y);
  return x + y;
}
5.2.2 箭頭函數(shù)的重要特性
  1. 不綁定 this

有經(jīng)驗的前端開發(fā)可能經(jīng)常會寫這樣的代碼 const self = this;const that = this; 看如下代碼:

class Dog {
  constructor(age) {
    this.age = age;
  }

  printAge() {
    console.log('output age: ', this.age);  // 輸出 10
    setTimeout(function inner() {
      this.age++;
      console.log('output age: ', this.age);  // 輸出 undefined
    }, 1000);
  }
}

let dog = new Dog(10);
dog.printAge();

在傳統(tǒng)定義的函數(shù)中,函數(shù)內(nèi)的 this 指向的是運行時的上下文環(huán)境须喂。例如上例代碼中的 inner 函數(shù)在 1 秒后運行吁断,運行時函數(shù)內(nèi)的 this 指向是運行時所在的環(huán)境對象即 window,由于 window.a 未定義坞生,所以最終輸出的是 undefined仔役。如果希望得到預(yù)期的結(jié)果,即對 Dog 實例的 age 屬性進行操作是己,則需要避開 this 關(guān)鍵字又兵,使用其他名稱來傳遞「定義時的上下文環(huán)境」,如下所示:

class Dog {
  constructor(age) {
    this.age = age;
  }

  printAge() {
    const that = this;  // 使用 that 傳遞 dog 實例上下文
    console.log('output age: ', this.age);  // 輸出 10

    setTimeout(function inner() {
      that.age++;
      console.log('output age: ', that.age);  // 輸出 11
    }, 1000);
  }
}

let dog = new Dog(10);
dog.printAge();

上述現(xiàn)象經(jīng)常被詬病卒废,但引入箭頭函數(shù)之后沛厨,由于箭頭函數(shù)不再在函數(shù)體內(nèi)定義和綁定 this ,所以在箭頭函數(shù)內(nèi)寫 this摔认,this 指向的就是定義箭頭函數(shù)所在的上下文逆皮,而不是運行時的上下文,如下所示:

class Dog {
  constructor(age) {
    this.age = age;
  }

  printAge() {
    console.log('output age: ', this.age);  // 輸出 10
    // this
    setTimeout(() => {
      this.age++;  // 由于箭頭函數(shù)內(nèi)部沒有 this级野,所以這里的 this 就是 printAge 函數(shù)的 this (也就是 dog 實例)
      console.log('output age: ', this.age);  // 輸出 11
    }, 1000);
  }
}

let dog = new Dog(10);
dog.printAge();
  1. 不綁定 arguments

普通函數(shù)內(nèi)页屠,實際傳入的參數(shù)會被綁定到一個內(nèi)置變量 arguments 中粹胯,我們可以從這個變量中獲取參數(shù):

const printAge = function(age) {
  console.log('age: ', arguments[0]);  // 輸出 age: 10
} 

printAge(10);

但箭頭函數(shù)不會綁定 arguments,如下所示:

const printAge = age => console.log('age: ', arguments[0]);  // 錯誤:arguments is not defined

printAge(10);
  1. 不可使用 yield

箭頭函數(shù)內(nèi)部不能使用 yield 即不可作為函數(shù)生成器 Generator辰企,有關(guān) Generator 可查閱下文的 Generator 部分风纠。

  1. 不可作為構(gòu)造函數(shù)

上面的第一點已經(jīng)提及箭頭函數(shù)缺乏 this,這自然導(dǎo)致箭頭函數(shù)不能作為構(gòu)造函數(shù)牢贸,不可搭配 new 關(guān)鍵字:

const Dog = () => 1;
let dog = new Dog();  // 錯誤竹观,箭頭函數(shù)不可作為構(gòu)造函數(shù),不可搭配 new 關(guān)鍵字

由于不能作為構(gòu)造函數(shù)潜索,所以也沒有相應(yīng)的 prototype 屬性:

const Dog = () => 1;
console.log('Dog prototype', Dog.prototype);  // undefined
  1. 不可使用 call()臭增、apply()bind() 等函數(shù)
    由于缺乏 this竹习,一些依賴于 this 的函數(shù)自然也不再適用:
this.age = 10;
const printAge1 = function() {
  console.log('age1: ', this.age); 
}

printAge1.call({age: 20});  // 輸出 age1: 20

const printAge2 =() => console.log('age2: ', this.age); 
printAge2.call({age: 20});  // 輸出 age2: 10 
5.3 尾調(diào)用優(yōu)化
5.3.1 何為尾調(diào)用

所謂的「尾調(diào)用」是指這樣的情形:一個函數(shù)的最后一步返回一個「函數(shù)調(diào)用」誊抛,看代碼會更加的清晰:

function funcA() {
  // ....
  return funcB();  // funcA 函數(shù)的尾部,調(diào)用了函數(shù) funcB整陌,并且直接返回了 funcB 的執(zhí)行結(jié)果
}

如果尾部進行了除函數(shù)調(diào)用外不純粹的動作拗窃,則不應(yīng)作為尾調(diào)用考慮:

function funcA() {
  // ....
  return funcB() + a; // 除了調(diào)用 funcB 之外,還需要將結(jié)果 + a 才返回
}

function funcA() {
  // ...
  let b = funcB();
  return b == 0 ? 1 : b;  // 除了調(diào)用 funcB泌辫,返回值還需要進行加工判斷
}

當(dāng)上面尾部函數(shù)調(diào)用為「自身調(diào)用」時随夸,此時的尾調(diào)用也就成為了「尾遞歸」。

5.3.2 何為尾調(diào)用優(yōu)化

我們知道一個函數(shù)調(diào)用的正常流程震放,例如:

function funcA() {
  return funcB();
}

當(dāng)執(zhí)行到 funcB() 代碼時宾毒,大體將進行如下步驟:

  1. 保存 funcA 函數(shù)的上下文環(huán)境到內(nèi)存棧
  2. 創(chuàng)建 funcB 函數(shù)所需要的上下文環(huán)境
  3. 切換到第二步創(chuàng)建的函數(shù)上下文環(huán)境去執(zhí)行
  4. 執(zhí)行完畢后回到棧中保存的 funcA 上下文

我們思考這樣一個問題:當(dāng) funcB 為尾調(diào)用時,我們是否還有必要再單獨創(chuàng)建一個新的上下文環(huán)境殿遂?

由于 funcB 函數(shù)執(zhí)行完畢后回到 funcA 后诈铛,僅剩的工作就是返回 funcB 的執(zhí)行結(jié)果,而這個過程中 funcA 的上下文環(huán)境中的大部分東西是不被需要的勉躺,那么完整的保存 funcA 的上下文環(huán)境也就沒有太大必要癌瘾。那么我們可以嘗試直接復(fù)用 funcA 的上下文環(huán)境,稍作修改將其作為 funcB 的上下文饵溅。于是步驟就變成:

  1. 如果 funcA 是尾調(diào)用妨退,那么執(zhí)行到最后一步時,直接修改 funcA 函數(shù)的上下文環(huán)境蜕企,將其作為 funcB 函數(shù)的執(zhí)行上下文
  2. funcB 執(zhí)行完畢后直接返回結(jié)果

這樣的優(yōu)化過程被稱為尾調(diào)用消除(Tail Call Elimination)尾調(diào)用優(yōu)化(Tail Call Optimization, TCO)咬荷。

尾調(diào)用優(yōu)化注意點:

  1. C++ 中的尾調(diào)用優(yōu)化
    在 C++ 中,在返回之前可能還涉及到返回值的析構(gòu)操作轻掩,所以 funcB 在 C++ 可能不是最后被執(zhí)行的函數(shù)幸乒,這樣也就無法應(yīng)用尾調(diào)用優(yōu)化,解決方案是應(yīng)用 C++ 的「返回值優(yōu)化」
  2. 尾調(diào)用優(yōu)化是否支持取決于編譯器或解釋器唇牧。目前并不是所有的編譯器或解釋器都支持此優(yōu)化罕扎。
5.4 函數(shù)的 name 屬性

ES6 之前聚唐,瀏覽器在實現(xiàn)時已經(jīng)提供了函數(shù)的 name 屬性,ES6 則是正式將其寫入標(biāo)準(zhǔn)中:

function funcA() {};  // name = "funcA"
const funcB = function funcA() {};  // name = "funcA"

// 與之前的瀏覽器實現(xiàn)略有不同的點:
const funcA = function() {};  // 不具名函數(shù)表達式腔召,ES5 中 name = ""杆查,ES6 中 name = "funcA"

#06 其它數(shù)據(jù)類型的擴展

6.1 Symbol

ES6 以前有六種基本數(shù)據(jù)類型:undefinednull臀蛛、布爾(Boolean)亲桦、數(shù)值(Number)逻谦、字符串(String)映屋、對象。ES6 引入了第七種基本數(shù)據(jù)類型 Symbol矩屁,Symbol 表示一個獨一無二的值抡柿,主要用于對象屬性的標(biāo)識舔琅。

在 ES6 之前,對象屬性存在產(chǎn)生沖突的可能洲劣,例如:

import { Dog } from 'dog.js';

Dog.sayHi = function() {/* ... */};  // Dog 內(nèi)部可能已經(jīng)擁有 sayHi 屬性

Symbol() 函數(shù)可以為我們生成一個獨一無二的 Symbol 值搏明,如:

let s = Symbol();

console.log(typeof s);  // "symbol"

可以傳入?yún)?shù)作為 Symbol 實例的描述,但同一個描述返回的仍然是不同的 Symbol 值:

let s1 = Symbol("dog");
let s2 = Symbol("dog");

console.log(s1 === s2); // false

有了 Symbol闪檬,我們并可為對象設(shè)置一個不會沖突的屬性:

import { Dog } from 'dog.js';

let s1 = Symbol("dog");
Dog[s1] = function() { console.log('say hi by dog.'); } ;

Dog[s1]();  // 調(diào)用

Symbol() 函數(shù)為我們返回的永遠是不同的值,但有時候我們需要得到同一個 Symbol 值购笆,這時候就需要另一個函數(shù) Symbol.for()粗悯。 Symbol.for(xxx) 會根據(jù)參數(shù) xxx 搜索對應(yīng)的 Symbol 值,如果還未存在則創(chuàng)建 xxx 對應(yīng)的 Symbol 值同欠,并將其注冊到全局環(huán)境以供搜索样傍。如果已經(jīng)存在,則直接返回 Symbol 值铺遂。這樣我們可以根據(jù)參數(shù)來得到同一個 Symbol 值:

let s1 = Symbol.for("dog");  // dog 對應(yīng)的 Symbol 還未存在衫哥,則創(chuàng)建并加入全局環(huán)境。
let s2 = Symbol.for("dog");  // 第二次調(diào)用襟锐, dog 對應(yīng)的 Symbol 已經(jīng)存在撤逢,則返回上一步創(chuàng)建的值

console.log(s1 === s2);  // true 

ES6 還提供了一系列的內(nèi)置 Symbol 值,用來表示一些內(nèi)部方法和內(nèi)部屬性粮坞,重寫這些方法可以改變對象的行為蚊荣,例如 Symbol.hasInstance,當(dāng)對某個對象使用 instanceof莫杈,實際調(diào)用的就是該函數(shù)互例,如下所示:

class MyArray {
  [Symbol.hasInstance](data) {
    return data instanceof Array;
  }
}

// 相當(dāng)于 MyArray[Symbol.hasInstance]([1, 2, 3]);
[1, 2, 3] instanceof new MyArray(); // 返回 true

類似的還有 Symbol.iteratorSymbol.split 等筝闹。

6.2 Set 和 Map

在其他語言中媳叨,SetMap 是兩種最為常見和常用的數(shù)據(jù)結(jié)構(gòu)腥光,ES6 為 JavaScritp 補充上了這種數(shù)據(jù)結(jié)構(gòu)。

6.2.1 Set
  1. 創(chuàng)建與基本使用

一種集合數(shù)據(jù)結(jié)構(gòu)糊秆,不允許存在重復(fù)值武福。創(chuàng)建一個 Set 數(shù)據(jù)結(jié)構(gòu)語法如下所示:

const mySet = new Set();

// 通過 add 函數(shù)添加元素
mySet.add(1); 
mySet.add(2);
mySet.add(1);  // 添加重復(fù)元素,重復(fù)元素會被「消除」

console.log(mySet);  //  1 2 
  1. 判等

Set 對于 NaN 能夠正確識別扩然,能夠正確判斷 NaNNaN 相等艘儒,也就是添加多次 NaN,Set 只會保存一個 NaN夫偶。同時兩個對象界睁,Set 永遠會判斷為不相等,如:

const mySet = new Set();

// 通過 add 函數(shù)添加元素
mySet.add({}); 
mySet.add({});

console.log(mySet);  //  含有兩個元素兵拢,{}, {}
  1. 初始化

可通過數(shù)組或任何具有 iterable 接口的數(shù)據(jù)結(jié)構(gòu)來初始化 Set:

const mySet1 = new Set([1, 2, 3, 1]);
console.log(mySet1); // 1 2 3
  1. 相關(guān)屬性和方法
  • add(value): 添加元素翻斟,返回整個 Set
  • delete(value): 刪除元素,返回刪除是否成功的布爾值
  • has(value): 判斷是否包含某個元素说铃,返回布爾值
  • clear() : 清空元素访惜,無返回值。
  • keys():返回所有鍵名腻扇,對于 Set 結(jié)構(gòu)债热,鍵名和鍵值相同,都是 Set 內(nèi)的元素值
  • values():返回所有鍵值幼苛,對于 Set 結(jié)構(gòu)窒篱,鍵名和鍵值相同,都是 Set 內(nèi)的元素值
  • entries():返回所有鍵值舶沿,對于 Set 結(jié)構(gòu)墙杯,鍵名和鍵值相同,都是 Set 內(nèi)的元素值括荡,entries 返回的 key 和 value 相同高镐,即兩個重復(fù)的元素值
  • size: 含有的元素個數(shù)

上述屬性和方法的實例代碼如下:

const mySet = new Set([1, 2,3]);
mySet.add(4);  // 1 2 3 4
console.log(mySet.delete(1));  // true,刪除后為 2 3 4
console.log(mySet.has(2));  // 存在 2畸冲,輸出 true
console.log(mySet.size);  // 3 個元素嫉髓,輸出 3

for (let key of mySet.keys()) {
  console.log(key); // 2 3 4
}

for (let value of mySet.values()) {
  console.log(value); // 2 3 4
}

for (let entry of mySet.entries()) {
  console.log(entry); // [2, 2] [3, 3] [4 4]
}

mySet.clear();  // 清空元素
6.2.2 Map

JavaScript 中的對象 Map 有一個特點,由鍵值對組成邑闲,所以有時對象可以作為一個簡單的 key-value 結(jié)構(gòu)來使用岩喷。但必須意識到對象作為 key-value 結(jié)構(gòu)使用是殘缺的,因為對象中的 key 只能以字符串的形式存在监憎。雖然在語法上可以傳入不同類型如布爾纱意、數(shù)值的數(shù)據(jù),但實際上只是被自動轉(zhuǎn)換成字符串類型:

const map = {};
map[1] = 'a';  // 實際上為 map['1']
map[true] = 'b';  // 實際上為 map['true']

console.log(map['1']);  // a
console.log(map['true']);  // b

當(dāng)傳入對象類型作為 key 時鲸阔,對象被字符串化為統(tǒng)一的 [object Object]

const map = {};
const obj1 = {a: 1};
const obj2 = {a: 2};
map[obj1] = 1;

console.log(map[obj1] === map[obj2]);  // true

由上不難看出對象作為 key-value 結(jié)構(gòu)是「不專業(yè)」的偷霉,所以 ES6 引入了更為專業(yè)且能夠支持各種數(shù)據(jù)類型作為 key 的 Map 結(jié)構(gòu)迄委。

  1. 創(chuàng)建與基本使用

創(chuàng)建一個 Map 結(jié)構(gòu)并使用如下所示:

const map = new Map();
const obj = {a: 1};

map.set(true, 1);
map.set('true', 2);
map.set(obj, 3);

console.log(map.get(true));  // 1
console.log(map.get('true'));  // 2
console.log(map.get(obj));  // 3
  1. 判等

Map 支持不同數(shù)據(jù)類型作為 key。對于數(shù)值类少、字符串叙身、布爾值類型,Map 通過嚴(yán)格相等 === 判斷是否是相同的 key 硫狞。且對于數(shù)組信轿、對象等,Map 通過地址來判斷是否是相同的 key残吩,也就是值完全相同的數(shù)組或?qū)ο蟛坪觯灰遣煌瑢嵗诓煌牡刂芬矔徽J(rèn)為不同的 key泣侮,如下所示:

const map = new Map();
map.set([1], 1);
console.log(map.get([1]));  // undefined 
  1. 初始化
    與 Set 類似即彪,Map 也可以接受任何具有 Iterator 接口的數(shù)據(jù)結(jié)構(gòu),且這個數(shù)據(jù)結(jié)構(gòu)中每個迭代到的元素需要是具有兩個元素的數(shù)組活尊。如一個二維數(shù)組:
const map = new Map([
  [true, 1],
  ['true', 2],
]);

console.log(map);  // true => 1, 'true' => 2
  1. 相關(guān)屬性和方法
  • set(key, value): 設(shè)置元素隶校,返回值為整個 Map。如果存在 key蛹锰,則會更新 value深胳,如果不存在 key,則新建 key铜犬。
  • get(key): 獲取 key 對應(yīng)的 value稠屠,key 不存在則返回 undefined
  • delete(key):刪除某個鍵值,返回刪除是否成功
  • has(key): 判斷是否包含某個鍵翎苫,返回布爾值
  • clear() : 清空元素,無返回值榨了。
  • keys():返回所有鍵名
  • values():返回所有鍵值
  • entries():返回所有鍵值對
  • size: 含有的元素個數(shù)

上述屬性和方法的實例代碼如下:

const map = new Map([
  ['a', 1],
  ['b', 2],
  ['c', 3],
]);

map.set('d', 4);
console.log(map.get('a'));    // 1
console.log(map.delete('a'));  // true煎谍,刪除后為 b => 2, c => 3, d => 4
console.log(map.has('d'));  // 存在 d,輸出 true
console.log(map.size);  // 3 個元素龙屉,輸出 3

for (let key of map.keys()) {
  console.log(key); // b c d
}

for (let value of map.values()) {
  console.log(value); // 2 3 4
}

for (let entry of map.entries()) {
  console.log(entry); // [b, 2] [c, 3] [d 4]
}

for (let [key, value] of map.entries()) {
  console.log(key ,value); // b 2呐粘、c 3、d 4
}

map.clear();  // 清空元素
6.3 字符串?dāng)U展
6.3.1 模板字符串

ES6 之前常見使用 + 拼接字符串转捕,這在一些網(wǎng)頁代碼拼接場景下會顯得非常的繁瑣作岖,ES6 引入了「模板字符串」:

const obj = 'world';
const str = `hello, ${obj}`;

模板字符串使用反引號 ` 符號來標(biāo)識,使用 ${} 符號來嵌入變量五芝。

模板字符串的一個重要特點就是可以保存空格和換行:

const str = `
<ul>
  <li>${name}</li>
  <li>${age}</li>
</ul>
`

模板字符串還可以跟在一個函數(shù)后面痘儡,這種模式被成為標(biāo)簽?zāi)0?/strong>:

const age = 123;
console.log`my age is ${age}!`;  // 函數(shù)名稱后面直接跟模板字符串

上面代碼中的模板字符串會被做一番處理形成參數(shù)再被傳入 console.log 函數(shù),所做處理如下:

  • 形成的第一個參數(shù)是一個數(shù)組枢步,里面每個元素為「非變量」部分沉删。例如上例中渐尿,第一個參數(shù)就是 ['my age is', ' !']
  • 形成的第二個、第三個... 后續(xù)參數(shù)為變量值矾瑰。例如上例中砖茸,第二個參數(shù)為 123
  • 第一個參數(shù)數(shù)組對象還會帶有一個 raw 屬性,里面保存原始字符串殴穴。

標(biāo)簽?zāi)0宓膽?yīng)用場景有過濾 HTML 字符串凉夯、多語言轉(zhuǎn)換等,標(biāo)簽?zāi)0蹇梢允惯@些實現(xiàn)變得更加直觀清晰:

SaferHTML`<p>${sender} has sent you a message.</p>`;  // SaferHTML 是對 HTML 字符串進行安全過濾的函數(shù)

i18n`HelloWorld.`; // 你好世界采幌。i18n為國際化處理函數(shù)
6.3.2 字符串遍歷器

ES6 實現(xiàn)了對字符串的遍歷器劲够,使用 for...of 語句來遍歷字符串:

for (const str of 'hello,world') {
  console.log(str);
}
6.3.3 includes(), startsWith(), endsWith(), repeat()

ES6 為字符串類型添加了三種方法:

  1. includes: 判斷字符串是否包含某個子字符串
"hello,world".includes("llo");  // 返回 true
"hello,world".includes("llo", 6); // 從下標(biāo) 6 開始查找,返回 false
  1. startsWith: 判斷字符串是否以某個子字符串開始
"hello,world".startsWith("hello");  // 返回 true
"hello,world".startsWith("hello", 2); // 從下標(biāo) 2 開始查找植榕,返回 false
  1. endsWith: 判斷字符串是否以某個子字符串結(jié)尾
"hello,world".endsWith("world");  // 返回 true
"hello,world".endsWith("world", 5); // 查找前 5 個字符再沧,判斷是否以 world 結(jié)尾
  1. repeat: 返回一個重復(fù) n 次的字符串
' i love you.'.repeat(3000);  // 愛你 3000 遍
6.4 數(shù)值擴展
6.4.1 二進制和八進制

ES6 提供二進制數(shù)值和八進制數(shù)值的表示,二進制使用 0b0B 表示尊残,八進制使用 0o0O 表示:

console.log(0b101 === 5); // true
console.log(0o701 === 449); // true

console.log(Number('0b101')); // 5, 使用 Number 將二進制轉(zhuǎn)換成十進制
console.log(Number('0o701'), 449);  // 449, 使用 Number 將八進制轉(zhuǎn)換成十進制
6.4.2 Number.parseInt(), Number.parseFloat()

ES6 之前 parseIntparseFloat 為全局變量炒瘸,ES6 開始將這兩個函數(shù)掛在 Number 下:

Number.parseInt('123');
Number.parseFloat('123.4');
6.5 數(shù)組擴展
6.5.1 spread 擴展運算符與數(shù)組

上文的 2.1.3 已經(jīng)引入了 rest 運算符 ...... 同時也可以作為 spread 擴展運算符寝衫。rest 運算符可以理解為將元素組織成數(shù)組顷扩,而 spread 運算符則是將數(shù)組擴展為元素。

spread 運算符作用于數(shù)組:

function funcA(a, b, c, d) {
  return a + b + c + d;
} 
const a = [1, 2, 3, 4];
funcA(...a);  // 將數(shù)組 [1, 2, 3, 4] 展開為 1 2 3 4

const copy_a = [...a];  // 拷貝數(shù)組 a
const [...copy_a] = a;  // 拷貝數(shù)組 a

const b = [..."123"]; // 作用于字符串慰毅,將其轉(zhuǎn)換成數(shù)組 ["1", "2", "3"]
const c = [5, 6, 7];

// 合并數(shù)組 a 和 數(shù)組 c隘截,得到數(shù)組 d [1, 2, 3, 4, 5, 6, 7]
// 注意是這里都是淺拷貝,如果數(shù)組 a 和數(shù)組 c 內(nèi)的元素為引用類型如對象汹胃,那么 d 指向的是 a 和 c 數(shù)組中的同一個元素
// 修改 a婶芭、c、d 中的引用類型元素都會影響到其他數(shù)組
const d = [...a, ...c];
6.5.2 fill()着饥、find() 犀农、findIndex()、Array.of()
  1. fill()

fill 函數(shù)用來填充一個數(shù)組宰掉,這在數(shù)組初始化的場景非常有用:

new Array(3).fill(0);  // [0, 0, 0];

fill 函數(shù)的第二個和第三個參數(shù)表示填充的起始地址和結(jié)束地址(不包含):

['a', 'b', 'c'].fill(0, 1, 2);  // 從 1 號下標(biāo)開始到 2 號下標(biāo)(不包含)呵哨,填充為 0

注意點:如果填充的是對象,則填充的是同一個對象轨奄,即進行的淺拷貝

const obj = {key: 'value'};
const arr = new Array(3).fill(obj);
arr[0].key = 'update value';  // 對第一個「對象」元素修改孟害,即對所有元素修改
console.log(arr);  // 輸出 [{key: 'update value'}, {key: 'update value'}, {key: 'update value'}]
  1. find()、findIndex()

find 函數(shù)從數(shù)組中查找某個符合條件的元素挪拟,并返回該元素挨务,如果未找到,則返回 undefined。其中條件通過 find 的「參數(shù)」(回調(diào)函數(shù))給出:

[1, 2, 3, 4, 5].find(function(value, index, arr) {
  // value: 遍歷到的元素
  // index: 遍歷到的下標(biāo)
  // arr: 完整數(shù)組
  return value === 5;  // 找到滿足值等于 5 的元素
});  // 返回 5

findIndex 和 find 函數(shù)類似耘子,只是返回的是元素的下標(biāo):

[1, 2, 3, 4, 5].findIndex(function(value, index, arr) {
  // value: 遍歷到的元素
  // index: 遍歷到的下標(biāo)
  // arr: 完整數(shù)組
  return value === 5;  // 找到滿足值等于 5 的元素
});  // 返回下標(biāo) 4

findIndex 比起 indexOf果漾,findIndex 解決了 indexOf 的一個潛在缺陷,即無法識別 NaN:

[1, NaN].indexOf(NaN);  // 返回 -1谷誓,即不存在
[1, NaN].findIndex(ele => Object.is(ele, NaN));  // 返回 1
  1. Array.of()

在 ES6 之前绒障,我們有時候會使用 Arraynew Array 來將一組值轉(zhuǎn)為數(shù)組:

const arr1 = new Array(1, 2, 3); // 得到 [1, 2, 3]
const arr2 = new Array();  // 得到 []

但要注意到 Array 構(gòu)造函數(shù)的重載,導(dǎo)致參數(shù)個數(shù)不同時捍歪,Array 構(gòu)造函數(shù)的語義有所不同户辱,當(dāng)只傳入一個參數(shù)時,這個參數(shù)表示數(shù)組的長度:

const arr3 = new Array(1);  // 得到 [empty]糙臼, 長度為 1庐镐,且元素為空

而 Array.of() 提供的則是沒有歧義的「將一組值轉(zhuǎn)換成數(shù)組」:

Array.of();  // []
Array.of(1, 2, 3);  // [1, 2, 3];
Array.of(1);  // [1]
6.5.3 entries(),keys() 和 values()

keys() 返回數(shù)組的鍵名(下標(biāo))变逃,values() 返回數(shù)組的鍵值必逆,entries() 返回鍵值對:

const arr = ['a', 'b', 'c'];
for (const idx of arr.keys()) {
  console.log(idx);  // 輸出下標(biāo) 0 1 2
}

for (const ele of arr.values()) {
  console.log(ele); // 輸出數(shù)組值 a b c
}

for (const [idx, ele] of arr.entries()) {
  console.log(idx, ele);  // 輸出 下標(biāo) 數(shù)組值,0 a 揽乱、1 b名眉、2 c
}
6.6 對象擴展
6.6.1 對象屬性簡寫

在 ES6 之前,定義一個對象如下:

const name = 'cat';
const age = 10;
const dog = {
  name: name,
  age: age,
  sayHi: function() {/* ... */}
};

ES6 對于上述屬性名和變量名相同的情形凰棉,可以簡寫如下:

const dog = {
  name,
  age,
  sayHi() {/* ... */}  // 注意 ES6 的函數(shù)也可以簡寫损拢,直接寫成函數(shù)定義即可
}
6.6.2 屬性名表達式

對象的屬性名有時候可能由表達式給出,例如:

const animal = {
  name: 'cat',
  age: 10,
  sayHi() {/* ... */}
}

const key = 'wangwang';
animal[key] = function() { console.log('i am a dog'); };
animal.wangwang();

在 ES6 以前撒犀,表達式給出的屬性名只能以 obj[xxx] 的形式定義福压,無法直接 {} 定義時給出,ES6 添加了對此的支持:

const key = 'wangwang';

const animal = {
  name: 'cat',
  age: 10,
  sayHi() {/* ... */},
  [key]: function() { console.log('i am a dog'); },  // 直接在 animal 定義時就實現(xiàn)表達式的屬性名
};

animal.wangwang();

注意如果上述的 key 是一個對象或舞,那么會自動轉(zhuǎn)換成字符串 [object Object]

const key1 = {name: 'wangwang'};
const key2 = {name: 'miaomiao'};
const animal = {
  name: 'cat',
  age: 10,
  sayHi() {/* ... */},
  [key1]: function() { console.log('i am a dog'); },  // [object Object]: function
  [key2]: function() { console.log('i am a cat'); },   // [object Object]: function 將會覆蓋上面的函數(shù)
};

上例代碼中 [key1][key2] 最終生成同名的 [object Object] 屬性荆姆,animal 最終只會有一個[object Object] 屬性,且對應(yīng)的函數(shù)為 i am a cat 函數(shù)映凳。

6.6.3 屬性遍歷

我們經(jīng)常需要對對象的屬性進行遍歷胆筒,一共有以下方法:

  • 表示包含,- 表示不包含
  1. for...in

能夠遍歷的屬性:繼承的屬性 + 自身可枚舉屬性 - Symbol 類型屬性

  1. Object.keys(obj)

能夠遍歷的屬性名:自身可枚舉屬性 - 繼承的屬性 - Symbol 類型屬性

  1. Object.getOwnPropertyNames(obj)

能夠遍歷的屬性: 自身所有屬性 - 繼承的屬性 - Symbol 類型屬性

  1. Object.getOwnPropertySymbols(obj)

能夠遍歷的屬性名:自身所有的 Symbol 類型屬性

  1. Reflect.ownKeys(obj)
    能夠遍歷的屬性: 自身所有屬性 + Symbol 類型屬性 - 繼承的屬性
6.6.4 Object.is()魏宽、Object.assign()
  1. Object.is()

JavaScript 有很多的坑,NaN 就是其中之一决乎。在 ES6 之前队询,我們使用 ===== 判斷兩個值是否相等時,總是需要特殊考慮 NaN 特例构诚。

ES6 引入了 Object.is() 來判斷兩個值是否相等蚌斩,而且對于 NaN 也能正確判斷:

Object.is(NaN, NaN);  // true
  1. Object.assign()

Object.assign 實現(xiàn)將源對象的可枚舉屬性復(fù)制到目標(biāo)對象:

const target = {a: 'a', b: 'b'};

const source1 = {c: 'c', d: 'd'};
const source2 = {e: 'e', f: 'f'};

const obj = Object.assign(target, source1, source2);

如果參數(shù) target, source1, source2, ... 中存在同名屬性,則規(guī)則為:后面的屬性覆蓋前面的屬性

#07 Promise

由于 JavaScript 單線程等特點范嘱,異步在 JavaScript 中可謂隨處可見送膳。尤其是在 Node.js 中员魏,更是將絕大多數(shù)的系統(tǒng)接口都設(shè)計為異步函數(shù)。

在 ES6 之前叠聋,JavaScript 主要通過「回調(diào)函數(shù)」的形式實現(xiàn)異步的結(jié)果返回撕阎。如果多個異步操作之間存在先后次序關(guān)系,就會產(chǎn)生了經(jīng)典的「回調(diào)地獄」:

/* getOne碌补、getTwo虏束、getMore 皆為模擬異步操作 */
/* 1 秒后通過回調(diào)告知操作結(jié)果 */

function getOne(params, callback) {
  setTimeout(() => {
    console.log(`oneResult: ${params}、1`);  // oneResult: 0厦章、1
    callback(`${params}镇匀、1`)
  }, 1000);
}

function getTwo(params, callback) {
    setTimeout(() => { 
      console.log(`twoResult: ${params}、2`);  // twoResult: 0袜啃、1汗侵、2
      callback(`${params}、2`)
    }, 1000);
}

function getMore(params, callback) {
    setTimeout(() => { 
      console.log(`moreResult: ${params}群发、3`);  // moreResult: 0晰韵、1、2也物、3
      callback(`${params}宫屠、3`)
    }, 1000);
}

// 上述異步操作存在先后順序
// 需要先執(zhí)行 getOne 得到 oneResult
// 然后將 oneResult 作為參數(shù)傳入 getTwo 執(zhí)行,獲取 twoResult
// 最后將 twoResult 作為參數(shù)傳入 getMore 執(zhí)行
// 則出現(xiàn)「回調(diào)函數(shù)」嵌套寫法如下
getOne(0, function(oneResult) {
  getTwo(oneResult, function(twoResult) {
    getMore(twoResult, function(moreResult) {
       console.log(`done.`);
    });
  });
});

「回調(diào)地獄」無疑使得代碼變得難以閱讀和編寫滑蚯,ES6 引入的 Promise 則可解決這一問題浪蹂。Promise 本質(zhì)是一個對象,該對象可以將一個「未來才會結(jié)束的事件」告材、「對該事件的回調(diào)處理」等封裝到一起坤次。這樣使得異步函數(shù)可以像同步函數(shù)那樣返回值。

7.1 基本語法

將上述的 getOne斥赋、getTwo缰猴、getMore 等異步操作通過 Promise 對象封裝:

function getOne(params) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => {
      console.log(`oneResult: ${params}、1`);  // oneResult: 0疤剑、1
      resolve(`${params}滑绒、1`)
    }, 1000);
  });
}

function getTwo(params) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => { 
      console.log(`twoResult: ${params}、2`);  // twoResult: 0隘膘、1疑故、2
      resolve(`${params}、2`)
    }, 1000);
  });
}

function getMore(params) {
  return new Promise(function(resolve, reject) {
    setTimeout(() => { 
      console.log(`moreResult: ${params}弯菊、3`);  // moreResult: 0纵势、1、2、3
      resolve(`${params}钦铁、3`)
    }, 1000);
  });
}

如上所示软舌, Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),且該函數(shù)會被傳入 resolve牛曹、 reject 兩個參數(shù)佛点。其中 resolve 函數(shù)的作用為將 Promise 對象的狀態(tài)從「未完成 pending」變?yōu)椤赋晒?resolved」,reject 函數(shù)的作用則是將 Promise 對象的狀態(tài)從「未完成 pending」變?yōu)椤甘?rejected」躏仇。

之后我們可以通過 Promise 實例的 then() 函數(shù)來綁定成功時回調(diào)函數(shù)和失敗時回調(diào)函數(shù):

// 綁定成功時回調(diào)函數(shù) sucCallBack恋脚,可選
// 綁定失敗時回調(diào)函數(shù) failCallback,可選
getOne.then(sucCallBack, failCallback);

Promise 是如何完成異步操作的焰手?整個過程可以簡單描述如下:

在我們通過 new 創(chuàng)建 promise 實例時糟描,傳入的函數(shù)會被立即執(zhí)行,例如上述的 setTimeout 代碼书妻。而其中異步的操作則會在「未來的某個時刻」返回結(jié)果船响,例如 setTimeout 設(shè)置的 1 秒后的回調(diào)函數(shù)。在該回調(diào)函數(shù)中我們調(diào)用了 resolve 函數(shù)躲履,將對象狀態(tài)從 pending 轉(zhuǎn)換為 resolved见间。這將觸發(fā) promise 實例調(diào)用我們通過 then() 綁定的處理函數(shù),其中傳入 resolve 函數(shù)的參數(shù)(通常為異步操作返回的結(jié)果)會被傳遞給我們綁定的處理函數(shù)進行處理工猜。reject 函數(shù)與 resolve 類似米诉。

上述過程由 Promise 對象在內(nèi)部通過「觀察者模式」和「狀態(tài)管理」(pending、resolved篷帅、rejected)來實現(xiàn)史侣,開發(fā)者只需懂得將異步通過 Promise 實例封裝,然后通過 then() 函數(shù)綁定回調(diào)函數(shù)即可魏身。

7.2 鏈?zhǔn)秸{(diào)用

我們在設(shè)置 Promise 實例的 then() 函數(shù)時惊橱,如果其中的返回值又是一個 Promise 對象,則可以在 .then() 緊接著跟一個 .then() 從而實現(xiàn)鏈?zhǔn)秸{(diào)用箭昵。 通過 Promise 鏈?zhǔn)秸{(diào)用并可解決「回調(diào)地獄」的問題:

getOne(0)
  .then(getTwo)  // getOne 執(zhí)行成功后并執(zhí)行回調(diào)「getTwo」
  .then(getMore)  // 由于 getTwo 返回的是一個 Promise税朴,所以后面又可以跟上 getMore,且在 getTwo 執(zhí)行成功后執(zhí)行回調(diào) getMore
  .then(() => console.log('done.'));  // getMore 執(zhí)行成功后執(zhí)行回調(diào)輸出 done.
7.3 Promise.prototype.catch

在 7.1 基本語法一節(jié)介紹了 .then() 函數(shù)的第二個參數(shù)為發(fā)生錯誤時或失敗時的「回調(diào)函數(shù) 」failCallBack家制。Promise 提供了 Promise.prototype.catch() 作為 .then(null, rejection).then(undefined, rejection) 的別名正林,以時調(diào)用語義更友好:

/* 調(diào)用 reject 會被 catch */
const promise1 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    const one = '1';
    reject(one);
  }, 1000);
});
promise1
  .then(() => console.log('callback'))  // 沒有輸出。因為上面代碼調(diào)用了 reject 所以屬于失敗颤殴,將調(diào)用失敗回調(diào) rejection 
  .catch((error) => console.log('error info: ', error));  // 輸出 error info:1觅廓。 catch 就是 .then(undefined, rejection) 別名

/* 調(diào)用 throw 一個 Error 也會被 catch */
const promise2 = new Promise(function(resolve, reject) {
  setTimeout(() => {
    const one = '1';
  }, 1000);
  throw new Error('1');
});
promise2
  .then(() => console.log('callback'))  // 沒有輸出。因為上面代碼調(diào)用了 reject 所以屬于失敗诅病,將調(diào)用失敗回調(diào) rejection 
  .catch((error) => console.log('error info: ', error));  // 輸出 error info:  Error: 1......飞蹂。 catch 就是 .then(undefined, rejection) 別名

Promise 對象的錯誤具有「冒泡」性質(zhì),會一直向后傳遞直到被捕獲:

const promise = new Promise(function(resolve, reject) {
  setTimeout(() => {
    const one = '1';
    resolve(one);
  }, 1000);
});
promise
  .then(() => x + 1)  // 此處拋出的錯誤會被最后的 .catch 捕獲 
  .then(() => 2)  // 如果此處有錯也會被最后的 .catch 捕獲
  .catch((error) => console.log('error info: ', error));  // 輸出 error info:  ReferenceError: x is not defined......

#08 Generator 函數(shù)

8.1 基本概念和用法

Generator 函數(shù)是 ES6 對協(xié)程的一種實現(xiàn)拄丰,所謂協(xié)程:

子程序就是協(xié)程的一種特例 —— 高德納 Donald Knuth

或者也可稱 「協(xié)程」為「子程序」的泛化吟孙,是對子程序概念的擴展。協(xié)程相關(guān)概念和知識可以查閱 Wikipedia 或其他資料芥永,這里暫時不做擴展篡殷。

由于 Generator 函數(shù)的執(zhí)行權(quán)恢復(fù)只可由 Generator 函數(shù)的調(diào)用者實現(xiàn),所以 Generator 函數(shù)也被成為「半?yún)f(xié)程[9]

[9] 一個完全的協(xié)程埋涧,任何函數(shù)都應(yīng)該具備能力使協(xié)程恢復(fù)執(zhí)行板辽。

Generator 函數(shù)需要在 function 關(guān)鍵字后跟隨一個星號 *,同時函數(shù)內(nèi)部通過 yield 命令實現(xiàn)執(zhí)行權(quán)主動讓出

// function 后面添加星號 *
// 以下寫法也是合法的
// function * lines
// function *lines
// function*lines
function* lines() {
  yield 'first';
  yield 'second';
  yield 'third';

  return 'EOF';
}

const generator = lines();  // 返回迭代器對象
generator.next();  // { value: 'first', done: false }
generator.next();  // { value: 'second', done: false }
generator.next();  // { value: 'third', done: false }
generator.next();  // { value: 'EOF', done: true }

如上所示棘催,一個 Generator 函數(shù)執(zhí)行時劲弦,不會立馬執(zhí)行函數(shù)體,而是返回一個「迭代器對象」醇坝,這個對象初始化指向函數(shù)頭部邑跪。

之后對這個迭代器調(diào)用 next() 將開始執(zhí)行函數(shù)體,執(zhí)行過程中遇到 yield 命令就會暫停呼猪,主動交出執(zhí)行權(quán)画畅,并將 yield 后面的值返回。

例如上例代碼中的第一次 .next() 調(diào)用后宋距,將從頭部開始執(zhí)行函數(shù)體轴踱,且在 yield 'first' 讓出執(zhí)行權(quán),將 'first' 返回谚赎,當(dāng)然返回值做了一定的封裝淫僻,返回的是一個對象 {value: xxx, done: xxx},其中 value 字段為 yield 后邊的值沸版,done 字段表示協(xié)程是否執(zhí)行完畢嘁傀。

調(diào)用一次 .next() 后,我們發(fā)現(xiàn)函數(shù)體只執(zhí)行一部分视粮。只有我們主動的繼續(xù)的調(diào)用 .next()细办,例如上例代碼的第二次 .next(),函數(shù)體才會繼續(xù)蕾殴,且函數(shù)會從上次退出的部分繼續(xù)執(zhí)行笑撞,第二次 .next() 執(zhí)行的則是 yield 'second',本次返回 { value: 'second', done: false }钓觉。之后第三次同理茴肥。直到第四次執(zhí)行 return 'EOF',返回值中的 done 被設(shè)置為 true荡灾,此時 Generator 函數(shù)才完全執(zhí)行完畢瓤狐。后續(xù)再調(diào)用 .next()瞬铸,只會返回 {value: undefined, done: true}

仔細體會上述 Generator 函數(shù)的執(zhí)行流程础锐,會發(fā)現(xiàn) Generator 函數(shù)有非常特殊的執(zhí)行流嗓节,這個過程中執(zhí)行權(quán)的切換由程序員定義,最終生成的也是一種程序員定義的「函數(shù)執(zhí)行流」皆警。再結(jié)合普通函數(shù)中只有一個return(或 異常)才能結(jié)束的單一執(zhí)行流拦宣,是不是可以理解為什么所謂協(xié)程就是「子程序」的泛化。

網(wǎng)上很多教程信姓,從「用戶態(tài)線程」的角度出發(fā)來理解協(xié)程其實并不嚴(yán)謹(jǐn)鸵隧,有可能導(dǎo)致很多理解偏差。理解協(xié)程本質(zhì)是泛化子程序之后意推,然后再去意識到協(xié)程通常被用于并發(fā)豆瘫,然后再思考被用于并發(fā)時本身具有哪些特點,與線程又有哪些區(qū)別等菊值。

我們注意到 yield 命令實際上起到一種將「函數(shù)內(nèi)的值」返回給「函數(shù)調(diào)用者」的作用靡羡,但我們直到函數(shù)之間交互必然還需要「函數(shù)調(diào)用者」將值傳遞到「函數(shù)內(nèi)」,則這一功能可以通過 .next(xxx) 來實現(xiàn)俊性。即通過 .next(xxx) 我們可以把數(shù)據(jù)傳遞給函數(shù):

function* lines() {
  yield 'first';
  yield 'second';
  const more = yield 'third';
  if (more) {
    yield 'fourth';
  }

  return 'EOF';
}

const generator = lines();  // 返回迭代器對象
generator.next();  // { value: 'first', done: false }
generator.next();  // { value: 'second', done: false }
generator.next();  // { value: 'third', done: false }
generator.next(true);  // 傳入?yún)?shù) true 給 more 略步,此時返回 { value: 'fourth', done: false }。如果傳入 false定页,則回跳過 yield 'fourth' 的執(zhí)行
generator.next(); // { value: 'EOF', done: true }

另外注意到 Generator 函數(shù)返回是一個迭代器對象趟薄,所以自然可以通過 for...of... 等操作遍歷:

for (const line of lines()) {
  console.log(line);  // first second third
}
8.2 Generator.prototype.throw

Generator 函數(shù)返回的迭代器對象還具有 throw() 函數(shù),用來表示 Generator 函數(shù)內(nèi)拋出異常:

function* lines() {
  yield 'first';
  yield 'second';
  const more = yield 'third';

  return 'EOF';
}

const generator = lines();  // 返回迭代器對象
generator.next();
generator.next();
generator.throw(new Error('出錯'));

thrownext 本質(zhì)上是一樣的典徊,都是向函數(shù)內(nèi)傳遞數(shù)據(jù)杭煎,只是 throw 傳入的是一個 Error,即上例代碼的 generator.throw(new Error('出錯')) 的效果相當(dāng)于:

function* lines() {
  yield 'first';
  yield 'second';

  // 這條語句將被替換 const more = throw(new Error('出錯'));
  const more = yield 'third';

  return 'EOF';
}

catch 到錯誤卒落,可以在 lines 函數(shù)內(nèi)進行 try...catch羡铲,也可以在函數(shù)外進行 try...catch

8.3 Generator.prototype.return

returnnext 同樣本質(zhì)上是一致的儡毕,只是替換的是return 語句:

function* lines() {
  yield 'first';
  yield 'second';
  const more = yield 'third'; // 這條語句將被替換 const more = return 2;

  return 'EOF';
}

const generator = lines();  // 返回迭代器對象
generator.next();
generator.next();
generator.return(2); // {value: 2, done: true}
generator.next();  // {value: undefined, done: true} 函數(shù)已經(jīng)結(jié)束了也切。
8.4 yield* 表達式

如果在 Generator 函數(shù)內(nèi)部,調(diào)用另一個 Generator 函數(shù)腰湾,我們可能需要手動遍歷:

function* lines() {
  yield 'first';
  yield 'second';
  const more = yield 'third'; // 這條語句將被替換 const more = return 2;

  return 'EOF';
}

function* files() {
  yield 'first file';
  // 遍歷 lines
  for (const line of lines()) {
    console.log(line);
  }
  return 'EOF';
}

如果有多重嵌套雷恃,那么實現(xiàn)起來就會非常麻煩。所以 ES6 引入了 yield* 表達式:

function* lines() {
  yield 'first';
  yield 'second';
  const more = yield 'third'; // 這條語句將被替換 const more = return 2;

  return 'EOF';
}

function* files() {
  yield 'first file';
  // yield* 表達式
  yield* lines();
  return 'EOF';
}
8.5 在異步編程上的應(yīng)用

Generator 函數(shù)最重要的應(yīng)用場景就是 JavaScript 中的異步編程费坊,例如實現(xiàn)一個典型的 AJAX 請求:

function* asyncByGenerator() {
  const result = yield ajax("/users", callback);  // 開始 ajax 異步請求倒槐,并不用等其結(jié)果立馬返回讓出執(zhí)行權(quán)
  const resp = JSON.parse(result);  // 這里是從 ajax 回調(diào)中 it.next(response) 處恢復(fù)的,此時已經(jīng)得到了異步結(jié)果附井,所以可以恢復(fù)繼續(xù)執(zhí)行
  console.log(resp.value);
}

function callback(response) {
    it.next(response);  // 在回調(diào)中恢復(fù)協(xié)程繼續(xù)執(zhí)行
}

// 獲得迭代對象
var it = asyncByGenerator();
it.next();  // 開始執(zhí)行

// it.next() 會立馬返回讨越,所以不影響做其他事情
console.log('hi');

上面 callback 實際上只是進行 next() 調(diào)用好讓協(xié)程恢復(fù)執(zhí)行两残,而業(yè)務(wù)的「回調(diào)邏輯代碼」完全可以寫到 yield 語句后面。換句話說 yield 后面的語句實際上才是真正的業(yè)務(wù)回調(diào)邏輯代碼把跨。同時上面的調(diào)用依賴于 it 全局變量磕昼,這顯然不是一個好的模式。

所以我們需要進一步封裝节猿,實現(xiàn)一個更為高級的 Generator 調(diào)度器:

function run(gen){
  let g = gen();

  function next(data){
    const result = g.next(data);

    if (result.done) return result.value;  // Generator 函數(shù)執(zhí)行完成,則返回
    
    // 重點漫雕,返回值為 Promise滨嘱,這樣我就可以在成功后「遞歸」調(diào)用 next
    // 當(dāng)然也可以直接返回回調(diào)函數(shù),對應(yīng)的技術(shù)稱為 Thunk 函數(shù)
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

function* asyncByGenerator() {
  const result = yield special("/users");  // 
  const resp = JSON.parse(result);
  console.log(resp.value);
}

run(asyncByGenerator);

run 就是 Generator 自動調(diào)度器浸间,他可以自動執(zhí)行一個 Generator 函數(shù)太雨,里面如果存在多個 yield,它將自動的依次執(zhí)行魁蒜。根本原理其實很簡單:執(zhí)行 next() 之后囊扳,他的返回值(也就是 yield 后面的表達式)必須是異步的回調(diào)函數(shù)(例如 ajax 異步操作的回調(diào)函數(shù))或 Promise。在這個回調(diào)函數(shù)里我再遞歸調(diào)用 next()兜看,這樣就可以在上一個 yield 執(zhí)行完成后繼續(xù)執(zhí)行下一個 yield 語句锥咸,直到 next() 返回 {done: true}

上例自動調(diào)度器代碼中在 next() 方法中對返回值進行了 .then(),即 result.value.then(...)细移,這里就要求 yield 后面的表達式必須返回的是一個 Promise 實例搏予。同時還有另一種寫法,即返回一個 Thunk 函數(shù)result.value(callback)弧轧。Thunk 函數(shù)相關(guān)知識可以查閱此 文檔 中的第四節(jié)雪侥。

現(xiàn)在已經(jīng)有一些第三方模塊實現(xiàn)封裝了類似上文介紹的 Generator 自動調(diào)度器,例如 co 模塊精绎。

當(dāng)然 JavaScript 官方在 ES2017 引入 async/await 語法糖來實現(xiàn)了上述能力速缨。

#09 Reflect

在編程領(lǐng)域,反射是一個耳熟能詳?shù)母拍畲恕S嘘P(guān)反射概念的簡單介紹可參閱深入 ProtoBuf - 反射原理解析 一文中的 [反射技術(shù)簡介] 一節(jié)旬牲。

JavaScript 由于是一門動態(tài)語言,反射的實現(xiàn)會更加自然搁吓。所以早在 ES6 之前引谜,JavaScript 中就已經(jīng)有一些反射相關(guān)的接口,如 Object. defineProperty 就可以在一個對象上定義一個新屬性或修改一個已有屬性擎浴。

ES6 為了實現(xiàn)更為規(guī)范和職責(zé)更為專一的入口员咽,設(shè)計了 Reflect 對象來提供所有反射相關(guān)的能力。

Reflect 對象一共提供了 13 種靜態(tài)方法來實現(xiàn)對象一系列的反射操作:

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • Reflect.set(target, name, value, receiver)
  • Reflect.defineProperty(target, name, desc)
  • Reflect.deleteProperty(target, name)
  • Reflect.has(target, name)
  • Reflect.ownKeys(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.getOwnPropertyDescriptor(target, name)
  • Reflect.getPrototypeOf(target)
  • Reflect.setPrototypeOf(target, prototype)

例如上述的 Reflect.has(target, name) 可以判斷某個對象是否存在某個屬性贮预,如下所示:

const dog = {
  name: 'wangwang',
  age: 5,
};

const attrName1 = 'age';
const attrName2 = 'master';
console.log(Reflect.has(dog, attrName1)); // true贝室,dog 對象存在 age 屬性
console.log(Reflect.has(dog, attrName2)); // false, dog 對象不存在 master 屬性

其它更多接口詳細說明可參閱 Reflect - From MDN

#10 Proxy

ES6 引入了 Proxy 對象契讲,可以用來創(chuàng)建對象的代理,從而實現(xiàn)對象屬性和方法的攔截(代理)滑频。

例如實現(xiàn)對一個對象所有屬性的訪問的代理如下所示:

const dog = {
  name: 'wangwang',
  age: 5,
};

// 使用 Proxy: 
// const proxy = new Proxy(target, handler);
const dogProxy = new Proxy(dog, {
  // target: 被代理的對象捡偏,這里為 dog
  // propKey: 訪問的屬性
  // receiver: 代理對象,這里為 dogProxy
  get: function (target, propKey, receiver) {
    console.log(`get prop [${propKey}] by proxy.`);
    // ... 例如打日志上報峡迷、計數(shù)等等更多操作
    return Reflect.get(target, propKey, receiver);
  },
});

console.log(dogProxy.name);  // get prop [name] by proxy. wangwang
console.log(dogProxy.age);  // get prop [age] by proxy. 5

上述代碼展示了 ES6 proxy 的基本用法:

const proxy = new Proxy(target, handler);

其中 target 表示被代理的對象银伟,如例子中的 doghandler 則是一個對象绘搞,其中有 13 種屬性[10]彤避,且屬性為函數(shù)類型,就是通過這些函數(shù)屬性來定義代理行為夯辖。例如上例中的 get 屬性琉预,定義屬性就是定義「訪問被代理對象的屬性」的代理函數(shù)。

[10]: 這里的屬性和上一節(jié) Reflect 的 13 種靜態(tài)方法是一一對應(yīng)的蒿褂。Reflect 實現(xiàn)針對對象的一系列操作圆米,那么 Proxy 自然會提供這一系列操作的代理。通常在代理函數(shù)中啄栓,通過 Reflect.xxx 就可以實現(xiàn)在完成相應(yīng)的代理操作后調(diào)用對象的原始行為了娄帖,例如上例代碼中進行日志輸出后通過 Reflect.get(...) 執(zhí)行原始行為即返回對象的屬性。

更多 handler 屬性如 set昙楚、has块茁、defineProperty 等可參閱 Proxy - From MDN

#11 Iterator 迭代器

迭代器在編程語言中是一種十分常見的接口,它主要用來為不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機制桂肌。ES6 中有關(guān) Iterator 的幾個概念如下:

  • Iterable: 一個數(shù)據(jù)結(jié)構(gòu)只要部署了 Symbol.iterator 屬性数焊,我們就可以稱之為 Iterable 即可迭代的。Symbol.iterator 屬性 為一個函數(shù)崎场,該函數(shù)應(yīng)該返回本數(shù)據(jù)結(jié)構(gòu)的迭代器 Iterator 對象佩耳。
class Iterable {
  constructor(data) {
    this.data = data
  }
  
  // 部署了 Symbol.iterator 屬性的數(shù)據(jù)結(jié)構(gòu)為 Iterable
  [Symbol.iterator]() {
    let index = 0;
    
    // Symbol.iterator 函數(shù)返回的是一個對象,該對象即為本數(shù)據(jù)結(jié)構(gòu)的 Iterator 對象
    return {
      next: () => {
        if (index < this.data.length) {
          return {value: this.data[index++], done: false}
        } else {
          return {done: true}
        }
      }
    }
  }
}
  • Iterator:通過 Symbol.iterator 函數(shù)返回的對象即為用來訪問數(shù)據(jù)結(jié)構(gòu)的 Iterator 對象谭跨。該對象通常一開始指向數(shù)據(jù)結(jié)構(gòu)的起始地址干厚,同時具有 next() 函數(shù),調(diào)用 next() 函數(shù)指向第一個元素螃宙,再次調(diào)用 next()函數(shù)指向第二個元素.... 重復(fù)并可迭代數(shù)據(jù)結(jié)構(gòu)中所有元素蛮瞄。
  • IteratorResultIterator 對象 每一次調(diào)用 next() 訪問的元素會被包裝返回,返回值為一個對象谆扎,其中包含 value 屬性表示元素值挂捅、done 屬性表示是否已經(jīng)迭代完成。

ES6 引入 for...of 語句對 Iterable 數(shù)據(jù)結(jié)構(gòu)進行迭代:

const myArray = new Iterable([1, 2, 3]);
for (const ele of myArray) {
  console.log(ele);
}

ES6 中的很多原生數(shù)據(jù)結(jié)構(gòu)已經(jīng)部署了 Symbol.iterator 屬性

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數(shù)的 arguments 對象
  • NodeList 對象

Array 為例堂湖,如下所示:

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();  // 獲取迭代器

console.log(iterator.next());  // {value: 1, done: false}
console.log(iterator.next());  // {value: 2, done: false}
console.log(iterator.next());  // {value: 3, done: true}

當(dāng)然可以直接使用 for...of 語句進行迭代:

const arr = [1, 2, 3];
for (const ele of arr) {
  console.log(ele);
}

參考資料

tc39 From GitHub
MDN Web Docs
ECMAScript 6 教程
Advanced ES6 Destructuring Techniques
The final feature set of ECMAScript 2016 (ES7)
ECMAScript 2017 (ES8): the final feature set
ECMAScript 2018: the final feature set
ES2018: asynchronous iteration
ECMAScript 2019: the final feature set
ECMAScript 2020: the final feature set

未汪

未汪闲先。
請查閱第二篇 ES6~ES11 特性介紹之 ES7~ES11 篇

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末状土,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子伺糠,更是在濱河造成了極大的恐慌蒙谓,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件训桶,死亡現(xiàn)場離奇詭異累驮,居然都是意外死亡,警方通過查閱死者的電腦和手機舵揭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門谤专,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人琉朽,你說我怎么就攤上這事≈上常” “怎么了箱叁?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惕医。 經(jīng)常有香客問我耕漱,道長,這世上最難降的妖魔是什么抬伺? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任螟够,我火速辦了婚禮,結(jié)果婚禮上峡钓,老公的妹妹穿的比我還像新娘妓笙。我一直安慰自己,他們只是感情好能岩,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布寞宫。 她就那樣靜靜地躺著,像睡著了一般拉鹃。 火紅的嫁衣襯著肌膚如雪辈赋。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天膏燕,我揣著相機與錄音钥屈,去河邊找鬼。 笑死坝辫,一個胖子當(dāng)著我的面吹牛篷就,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播近忙,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼腻脏,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了做鹰?” 一聲冷哼從身側(cè)響起鼎姐,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钾麸,失蹤者是張志新(化名)和其女友劉穎炕桨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體献宫,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡钥平,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年涉瘾,在試婚紗的時候發(fā)現(xiàn)自己被綠了捷兰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡秘蛇,死狀恐怖顶考,靈堂內(nèi)的尸體忽然破棺而出驹沿,到底是詐尸還是另有隱情,我是刑警寧澤甚负,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布梭域,位于F島的核電站,受9級特大地震影響富玷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜雀鹃,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望励两。 院中可真熱鬧黎茎,春花似錦、人聲如沸当悔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盲憎。三九已至嗅骄,卻和暖如春溺森,著一層夾襖步出監(jiān)牢的瞬間屏积,已是汗流浹背伸但。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工更胖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留却妨,地道東北人括眠。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓掷豺,卻偏偏與公主長得像当船,于是被迫代替她去往敵國和親德频。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

推薦閱讀更多精彩內(nèi)容