原本想稍微整理一下 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è)計上的缺陷。如塊級作用域的缺失喇完、模塊化的缺失伦泥、
null
和undefined
的設(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.0
到 ECMAScript 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)象:
- 內(nèi)層變量覆蓋外層變量
function funcA() {
var a = 1;
if (true) {
var a = 2;
}
console.log(a); // 輸出為 2
}
- 循環(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)賦值都是配合 let
,const
進行的民镜,即聲明和解構(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();
柠掂,這時候 dog1
為 undefined
项滑,因為 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)鍵:
- 構(gòu)造函數(shù)具有
prototype
屬性氧急,指向構(gòu)造函數(shù)對應(yīng)的原型(可理解為其他語言中的「類」) - 所有通過構(gòu)造函數(shù)創(chuàng)建的實例對象颗胡,會存在一個默認(rèn)屬性指向上述的原型(瀏覽器在具體實現(xiàn)時這個屬性通常命名為 proto)
- 上述的原型本身又會有
__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
等一樣吧秕,約束了變量提升。
class
和 function
一樣有表達式的寫法:
// 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 的一些特性
-
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)出的是對象類型,可以在該類型上添加屬性。但不建議這么做
-
import
具有提升效果:
handleDog(); // 合法,因為下面的 import 語句在編譯期(語句分析階段)就被執(zhí)行了寿桨,早于 `handleDog 函數(shù)` 的調(diào)用此衅。
import { name, another_name, handleDog, handleCat } from './utils.js';
-
import
編譯期(語句分析階段)執(zhí)行,所以是「靜態(tài)」的亭螟,無法使用表達式墨微、變量等運行時才可確定的值:
import { 'n' + 'ame' } from './utils.js'; // 錯誤
const module_name = './utils.js';
import { name } from module_name; // 錯誤
- 重復(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';
- 導(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ā)的作用裆操。此時可以使用上文介紹的 export
和 import
語法混合在一起的特殊語法:
// 「導(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)值之后,有幾點改變需要特別注意:
-
函數(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
- 參數(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ù)的重要特性
- 不綁定
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();
- 不綁定 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);
- 不可使用 yield
箭頭函數(shù)內(nèi)部不能使用 yield
即不可作為函數(shù)生成器 Generator辰企,有關(guān) Generator 可查閱下文的 Generator 部分风纠。
- 不可作為構(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
- 不可使用
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()
代碼時宾毒,大體將進行如下步驟:
- 保存 funcA 函數(shù)的上下文環(huán)境到內(nèi)存棧
- 創(chuàng)建 funcB 函數(shù)所需要的上下文環(huán)境
- 切換到第二步創(chuàng)建的函數(shù)上下文環(huán)境去執(zhí)行
- 執(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 的上下文饵溅。于是步驟就變成:
- 如果 funcA 是尾調(diào)用妨退,那么執(zhí)行到最后一步時,直接修改 funcA 函數(shù)的上下文環(huán)境蜕企,將其作為 funcB 函數(shù)的執(zhí)行上下文
- funcB 執(zhí)行完畢后直接返回結(jié)果
這樣的優(yōu)化過程被稱為尾調(diào)用消除(Tail Call Elimination)或尾調(diào)用優(yōu)化(Tail Call Optimization, TCO)咬荷。
尾調(diào)用優(yōu)化注意點:
-
C++ 中的尾調(diào)用優(yōu)化
在 C++ 中,在返回之前可能還涉及到返回值的析構(gòu)操作轻掩,所以funcB
在 C++ 可能不是最后被執(zhí)行的函數(shù)幸乒,這樣也就無法應(yīng)用尾調(diào)用優(yōu)化,解決方案是應(yīng)用 C++ 的「返回值優(yōu)化」 - 尾調(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ù)類型:undefined
、null
臀蛛、布爾(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.iterator
、Symbol.split
等筝闹。
6.2 Set 和 Map
在其他語言中媳叨,Set
和 Map
是兩種最為常見和常用的數(shù)據(jù)結(jié)構(gòu)腥光,ES6 為 JavaScritp 補充上了這種數(shù)據(jù)結(jié)構(gòu)。
6.2.1 Set
- 創(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
- 判等
Set 對于 NaN
能夠正確識別扩然,能夠正確判斷 NaN
和 NaN
相等艘儒,也就是添加多次 NaN
,Set 只會保存一個 NaN
夫偶。同時兩個對象界睁,Set 永遠會判斷為不相等,如:
const mySet = new Set();
// 通過 add 函數(shù)添加元素
mySet.add({});
mySet.add({});
console.log(mySet); // 含有兩個元素兵拢,{}, {}
- 初始化
可通過數(shù)組或任何具有 iterable
接口的數(shù)據(jù)結(jié)構(gòu)來初始化 Set:
const mySet1 = new Set([1, 2, 3, 1]);
console.log(mySet1); // 1 2 3
- 相關(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)迄委。
- 創(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
- 判等
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
-
初始化
與 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
- 相關(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 為字符串類型添加了三種方法:
- includes: 判斷字符串是否包含某個子字符串
"hello,world".includes("llo"); // 返回 true
"hello,world".includes("llo", 6); // 從下標(biāo) 6 開始查找,返回 false
- startsWith: 判斷字符串是否以某個子字符串開始
"hello,world".startsWith("hello"); // 返回 true
"hello,world".startsWith("hello", 2); // 從下標(biāo) 2 開始查找植榕,返回 false
- endsWith: 判斷字符串是否以某個子字符串結(jié)尾
"hello,world".endsWith("world"); // 返回 true
"hello,world".endsWith("world", 5); // 查找前 5 個字符再沧,判斷是否以 world 結(jié)尾
- repeat: 返回一個重復(fù) n 次的字符串
' i love you.'.repeat(3000); // 愛你 3000 遍
6.4 數(shù)值擴展
6.4.1 二進制和八進制
ES6 提供二進制數(shù)值和八進制數(shù)值的表示,二進制使用 0b
或 0B
表示尊残,八進制使用 0o
或 0O
表示:
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 之前 parseInt
和 parseFloat
為全局變量炒瘸,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()
- 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'}]
- 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
- Array.of()
在 ES6 之前绒障,我們有時候會使用 Array
或 new 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)常需要對對象的屬性進行遍歷胆筒,一共有以下方法:
- 表示包含,- 表示不包含
- for...in
能夠遍歷的屬性:繼承的屬性 + 自身可枚舉屬性 - Symbol 類型屬性
- Object.keys(obj)
能夠遍歷的屬性名:自身可枚舉屬性 - 繼承的屬性 - Symbol 類型屬性
- Object.getOwnPropertyNames(obj)
能夠遍歷的屬性: 自身所有屬性 - 繼承的屬性 - Symbol 類型屬性
- Object.getOwnPropertySymbols(obj)
能夠遍歷的屬性名:自身所有的 Symbol 類型屬性
-
Reflect.ownKeys(obj)
能夠遍歷的屬性: 自身所有屬性 + Symbol 類型屬性 - 繼承的屬性
6.6.4 Object.is()魏宽、Object.assign()
- Object.is()
JavaScript 有很多的坑,NaN
就是其中之一决乎。在 ES6 之前队询,我們使用 ==
或 ===
判斷兩個值是否相等時,總是需要特殊考慮 NaN
特例构诚。
ES6 引入了 Object.is()
來判斷兩個值是否相等蚌斩,而且對于 NaN 也能正確判斷:
Object.is(NaN, NaN); // true
- 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('出錯'));
throw
和 next
本質(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
return
和 next
同樣本質(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
表示被代理的對象银伟,如例子中的 dog
,handler
則是一個對象绘搞,其中有 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)中所有元素蛮瞄。 -
IteratorResult:
Iterator 對象
每一次調(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 篇