一門(mén)語(yǔ)言,無(wú)論是機(jī)器語(yǔ)言蚀乔,還是人類(lèi)語(yǔ)言烁竭。在學(xué)習(xí)的時(shí)候,我們從入門(mén)到精通會(huì)在以下幾個(gè)角度做分別的深入:
- 詞法
- 語(yǔ)法
- 句法
對(duì)于 JavaScript核心而言吉挣,有些人喜歡將 ECMAScript 看作是 JavaScript 的詞法核心派撕,規(guī)定了其內(nèi)置的語(yǔ)言特性,關(guān)鍵詞和基本邏輯的構(gòu)造過(guò)程睬魂。
這些基本句法相對(duì)固定终吼,加之在日常開(kāi)發(fā)中我們反復(fù)練習(xí),并無(wú)太多出入之分氯哮。區(qū)別大多在于孰能生巧記得住和光說(shuō)不練假把式际跪。而與之相對(duì)的,圍繞對(duì)象展開(kāi)的 JavaScript 語(yǔ)言特性部分喉钢,結(jié)合了面向?qū)ο蟮臉?gòu)造過(guò)程垫卤,引入大量中英文互譯中模棱兩可的概念。導(dǎo)致部分工程師并不知其所以然出牧。
JavaScript 每一次進(jìn)步穴肘,都值得一線(xiàn)工程師來(lái)修正處于語(yǔ)言特性核心的東西。在這篇之前舔痕,相關(guān)的知識(shí)碎片化嚴(yán)重评抚。
說(shuō)到核心,可以理解為精粹伯复,當(dāng)然更重要的是中心理論中的重點(diǎn)難點(diǎn)慨代。
[TOC]
- 作用域鏈
- 閉包
- this
- 執(zhí)行上下文
- 對(duì)象
- 變量對(duì)象
- 活動(dòng)對(duì)象
- 原型鏈
- 繼承
理解這些關(guān)鍵詞之前,首先需要明白兩件事
- JavaScript 沒(méi)有類(lèi)的概念啸如,所以也沒(méi)有類(lèi)概念中的封裝侍匙,繼承,多態(tài)
- 引入與類(lèi)相似的概念的目的叮雳,是為了實(shí)現(xiàn)代碼重用
對(duì)象
對(duì)象是 JavaScript 中的引用類(lèi)型想暗。
JavaScript中妇汗,一個(gè)對(duì)象就是一個(gè)屬性和方法的集合,每一個(gè)對(duì)象都擁有一個(gè)
原型對(duì)象
说莫,其本身也是個(gè)對(duì)象杨箭。
構(gòu)造一個(gè)對(duì)象常見(jiàn)方式有三種:對(duì)象字面直接量,使用 new 關(guān)鍵字構(gòu)建储狭,使用Object.create()方法互婿。
為了優(yōu)化構(gòu)造對(duì)象的過(guò)程(解決對(duì)象的來(lái)源,對(duì)象構(gòu)造的重復(fù)問(wèn)題辽狈,對(duì)象構(gòu)造的重用性問(wèn)題)引入一部分設(shè)計(jì)模式
- 工廠(chǎng)模式(P 批量構(gòu)造 N 看不出來(lái)源)
- 構(gòu)造函數(shù)(P 聲明來(lái)源 N 資源浪費(fèi))
- 原型模式(P 實(shí)現(xiàn)重用 N 相互影響)
拿字面直接量方式構(gòu)造來(lái)說(shuō)明原型對(duì)象的存在慈参。
var foo = {
x: 10,
y:20
}
就構(gòu)成了如下對(duì)象與對(duì)象所具有的原型對(duì)象的關(guān)聯(lián)關(guān)系。如圖(1)
其中 __proto__
已被各大瀏覽器棄用刮萌,此屬性的值用來(lái)存儲(chǔ)內(nèi)部指向自己所對(duì)應(yīng)原型對(duì)象的指針驮配。
原型對(duì)象有什么用呢?我們用原型鏈來(lái)解釋尊勿。
原型鏈
原型對(duì)象也是對(duì)象,也擁有自己的 __proto__
屬性指向自己的原型對(duì)象畜侦,這種串聯(lián)多個(gè)原型對(duì)象的模式叫做原型鏈元扔。
原型鏈?zhǔn)怯脕?lái)實(shí)現(xiàn)繼承和屬性共享的有限對(duì)象鏈
這里提出了 繼承
的概念,如果你沒(méi)有 OOP 的概念旋膳,那恭喜你了澎语,你能很快理解這種便捷的方式和概念,如果你是 Java/PHP 出身验懊,你直接把這里的 繼承
理解為 重用吧擅羞。
JavaScript 引入繼承概念,就是為了實(shí)現(xiàn)屬性和方法的重用义图。
如果一個(gè)屬性或方法在自身中無(wú)法找到减俏,那么會(huì)進(jìn)入原型鏈查找這個(gè)屬性或方法,依次遍歷整個(gè)鏈碱工,第一個(gè)被查找到的將會(huì)使用
如果沒(méi)有明確的指明原型對(duì)象的指向娃承,那么原型對(duì)象的原型指針會(huì)指向 Object 的原型對(duì)象,而后者的原型對(duì)象指針指向 null怕篷,也就是原型鏈的重點(diǎn)历筝。
舉個(gè) ??,有三個(gè)對(duì)象廊谓,b,c 對(duì) a 有不同程度繼承梳猪,代碼如下
let a = {
x: 10,
calc(z){return this.x+this.y+z }
}
let b = {
y:20,
__proto__: a
}
let c = {
y:30,
__proto__: a
}
b.calc(30);//?
c.calc(40);//?
這段代碼簡(jiǎn)明扼要,通過(guò) __proto__
代指原型鏈的鏈接過(guò)程蒸痹,實(shí)際實(shí)現(xiàn)有所不同春弥。由此構(gòu)成了 a,b,c 之間的原型鏈如下圖(2)
上述例子中呛哟,重用了很多魔術(shù)變量,實(shí)際實(shí)現(xiàn)繼承的過(guò)程惕稻,我們對(duì)類(lèi)的希望是抽象的 AST結(jié)構(gòu)竖共,擁有相同或相似的狀態(tài)結(jié)構(gòu),不同的狀態(tài)值和方法俺祠。于是公给,要引入構(gòu)造函數(shù)對(duì)類(lèi)進(jìn)行初始化。
構(gòu)造函數(shù)
由構(gòu)造函數(shù)組成的類(lèi)型蜘渣,我們使用 new 關(guān)鍵字新建一個(gè)新的對(duì)象淌铐。用這個(gè)方式重寫(xiě)上邊 abc 的例子,重用屬性和方法的時(shí)候使用構(gòu)造函數(shù)+原型模式蔫缸。
function Foo(y) {
this.y = y;
}
Foo.prototype.x = 10;
Foo.prototype.calculate = function (z) { return this.x + this.y + z; };
var b = new Foo(20);
var c = new Foo(30);
b.calculate(30); // ?
c.calculate(40); // ?
b.constructor === Foo, // ?
c.constructor === Foo, // ?
Foo.prototype.constructor === Foo // ?
上邊代碼改進(jìn)后腿准,可以看到多了兩個(gè)關(guān)鍵詞,在 Foo 構(gòu)造函數(shù)的原型對(duì)象中有一個(gè) contstructor
屬性指回了構(gòu)造函數(shù)本身拾碌。代碼邏輯圖如圖(3)所示
構(gòu)造函數(shù)+原型對(duì)象 合在一起吐葱,被我們稱(chēng)為 JavaScript 中的類(lèi)。
ES6 對(duì)類(lèi)的封裝過(guò)程進(jìn)行了優(yōu)化校翔,引入 class
extends
super
等關(guān)鍵字弟跑,其實(shí)質(zhì)還是基于原型鏈的委托繼承又叫原型繼承。
執(zhí)行上下文
JavaScript 在 ES6之前是沒(méi)有塊級(jí)作用域的防症,基于這樣的人設(shè)孟辑,每段代碼都在自己的上下文環(huán)境中進(jìn)行求值。此時(shí)函數(shù)作用域就被認(rèn)為是局部作用域或者塊級(jí)作用域蔫敲。
JavaScript 是通過(guò)棧結(jié)構(gòu)來(lái)管理保存系統(tǒng)運(yùn)行時(shí)的上下文狀態(tài)轉(zhuǎn)換的饲嗽,稱(chēng)為執(zhí)行上下文棧。堆棧頂部的執(zhí)行上下文稱(chēng)為活動(dòng)上下文奈嘿。
觸發(fā)上下文堆棧中其他上下文的執(zhí)行上下文稱(chēng)作 caller
貌虾,被觸發(fā)的執(zhí)行上下文稱(chēng)為callee
,從函數(shù)角度更容易理解裙犹,執(zhí)行函數(shù)叫做 caller酝惧,函數(shù)的容器稱(chēng)為 callee。
一個(gè)局部作用域生效時(shí)伯诬,將其壓入執(zhí)行上下文堆棧晚唇,作為活動(dòng)執(zhí)行上下文
執(zhí)行上下文在 JavaScript 中也被實(shí)現(xiàn)為對(duì)象。對(duì)象中包含追蹤相關(guān)代碼執(zhí)行過(guò)程的屬性盗似。常見(jiàn)的幾個(gè)屬性有
- 變量對(duì)象
- 作用域鏈
- this 指針
變量對(duì)象
變量對(duì)象是一個(gè)抽象概念哩陕,變量對(duì)象中存儲(chǔ)了在當(dāng)前上下文中存儲(chǔ)的變量和函數(shù)聲明灵份。
函數(shù)表達(dá)式不包含在變量對(duì)象之中荠察。
在全局執(zhí)行上下文中翼馆,變量對(duì)象就是全局對(duì)象本身埃跷,考慮以下這個(gè)例子。
var foo = 10;
function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE
console.log( this.foo == foo); // true
console.log( window.bar == bar );// true
console.log(baz); // ReferenceError, "baz" is not defined
此時(shí)心赶,全局變量對(duì)象的數(shù)據(jù)結(jié)構(gòu)如圖(4)所示
其中函數(shù)表達(dá)式(閉包)并未進(jìn)入全局變量對(duì)象扣讼,所以在全局調(diào)用也會(huì)失敗。
當(dāng)一個(gè)變量對(duì)象進(jìn)入執(zhí)行時(shí)缨叫,稱(chēng)作活動(dòng)對(duì)象椭符。
活動(dòng)對(duì)象
活動(dòng)對(duì)象是特殊的變量對(duì)象,也是實(shí)際存在的對(duì)象耻姥,在函數(shù)被觸發(fā)的時(shí)候創(chuàng)建销钝,活動(dòng)對(duì)象中默認(rèn)包含 形參和arguments 對(duì)象。
函數(shù)表達(dá)式不在變量對(duì)象中琐簇,所以也不在活動(dòng)對(duì)象中
函數(shù)在運(yùn)行過(guò)程中蒸健,不僅可以使用活動(dòng)對(duì)象中的屬性和方法,還可以使用父容器的屬性和方法婉商。這種可以使用的實(shí)現(xiàn)原理就基于執(zhí)行上下文的第二個(gè)屬性似忧,作用域鏈。
作用域鏈
作用域鏈?zhǔn)遣檎易兞恐档逆準(zhǔn)浇Y(jié)構(gòu)丈秩,是一個(gè)對(duì)象列表盯捌。其實(shí)現(xiàn)原理與原型鏈相似。
如果一個(gè)變量在當(dāng)前作用域中未找到定義癣籽,則遞歸上溯到父容器中查找挽唉,直到查找到作用域鏈的尾部滤祖。未找到返回
undefinded
如果函數(shù)引用了一個(gè)不是當(dāng)前上下文中的標(biāo)識(shí)符(變量筷狼,參數(shù),方法)那么被引用的這個(gè)標(biāo)識(shí)符被稱(chēng)作自由變量匠童,搜索自由變量的過(guò)程就是依次遍歷作用域鏈的過(guò)程埂材。
看一個(gè)例子
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
console.log(x + y + z);
})();
})();
其中三個(gè)變量 x,y,z 在調(diào)用時(shí)發(fā)現(xiàn),x,y 針對(duì) bar 而講屬于自由變量汤求,需要搜索作用域鏈獲取值俏险。其搜索過(guò)程如圖(5)所示。
當(dāng)使用 with/catch 語(yǔ)句時(shí)扬绪,將其中的語(yǔ)句插入作用域鏈的前端竖独,使得被插入的片段既包含proto屬性又包含parent屬性,原型鏈的查找邏輯中優(yōu)先查找proto屬性鏈挤牛。
活動(dòng)變量在函數(shù)執(zhí)行完畢后莹痢,將交付垃圾回收機(jī)制回收,如果不想被回收掉。那么就需要引入閉包的概念竞膳。
閉包
閉包是為了讓函數(shù)成為一等公民航瞭,解決函數(shù)作為參數(shù)和函數(shù)作為返回值時(shí)作用域鏈存活的問(wèn)題。
當(dāng)函數(shù)作為參數(shù)或返回值時(shí)坦辟,函數(shù)中自由變量訪(fǎng)問(wèn)的容器函數(shù)尚可訪(fǎng)問(wèn)(未被銷(xiāo)毀)刊侯,該函數(shù)會(huì)在創(chuàng)建的時(shí)候,保存容器函數(shù)的作用域鏈锉走。
閉包創(chuàng)建的函數(shù)作用域鏈 = 活動(dòng)對(duì)象 + 父函數(shù)作用域鏈
保存是為了未來(lái)訪(fǎng)問(wèn)的時(shí)候能夠訪(fǎng)問(wèn)到滨彻。此時(shí),父函數(shù)的作用域鏈被調(diào)用函數(shù)凍結(jié)了挠日,稱(chēng)此為靜態(tài)作用域疮绷。靜態(tài)作用域是一門(mén)語(yǔ)言能夠創(chuàng)造閉包的必需條件,JavaScript 就具備這個(gè)條件嚣潜,現(xiàn)在給閉包下一個(gè)準(zhǔn)確的定義:
閉包是一個(gè)方便查找自由變量的代碼塊冬骚,以塊級(jí)作用域?yàn)榛A(chǔ)構(gòu)造靜態(tài)作用域,以保存父容器作用域鏈的集合體懂算。
很抱歉只冻,又說(shuō)迷糊了。什么是閉包计技?JavaScript 中所有函數(shù)都是閉包喜德。