一門語言,無論是機器語言,還是人類語言。在學習的時候,我們從入門到精通會在以下幾個角度做分別的深入:
- 詞法
- 語法
- 句法
對于 JavaScript核心而言,有些人喜歡將 ECMAScript 看作是 JavaScript 的詞法核心,規定了其內置的語言特性,關鍵詞和基本邏輯的構造過程。
這些基本句法相對固定,加之在日常開發中我們反復練習,并無太多出入之分。區別大多在于孰能生巧記得住和光說不練假把式。而與之相對的,圍繞對象展開的 JavaScript 語言特性部分,結合了面向對象的構造過程,引入大量中英文互譯中模棱兩可的概念。導致部分工程師并不知其所以然。
JavaScript 每一次進步,都值得一線工程師來修正處于語言特性核心的東西。在這篇之前,相關的知識碎片化嚴重。
說到核心,可以理解為精粹,當然更重要的是中心理論中的重點難點。
[TOC]
- 作用域鏈
- 閉包
- this
- 執行上下文
- 對象
- 變量對象
- 活動對象
- 原型鏈
- 繼承
理解這些關鍵詞之前,首先需要明白兩件事
- JavaScript 沒有類的概念,所以也沒有類概念中的封裝,繼承,多態
- 引入與類相似的概念的目的,是為了實現代碼重用
對象
對象是 JavaScript 中的引用類型。
JavaScript中,一個對象就是一個屬性和方法的集合,每一個對象都擁有一個
原型對象
,其本身也是個對象。
構造一個對象常見方式有三種:對象字面直接量,使用 new 關鍵字構建,使用Object.create()方法。
為了優化構造對象的過程(解決對象的來源,對象構造的重復問題,對象構造的重用性問題)引入一部分設計模式
- 工廠模式(P 批量構造 N 看不出來源)
- 構造函數(P 聲明來源 N 資源浪費)
- 原型模式(P 實現重用 N 相互影響)
拿字面直接量方式構造來說明原型對象的存在。
var foo = {
x: 10,
y:20
}
就構成了如下對象與對象所具有的原型對象的關聯關系。如圖(1)
其中 __proto__
已被各大瀏覽器棄用,此屬性的值用來存儲內部指向自己所對應原型對象的指針。
原型對象有什么用呢?我們用原型鏈來解釋。
原型鏈
原型對象也是對象,也擁有自己的 __proto__
屬性指向自己的原型對象,這種串聯多個原型對象的模式叫做原型鏈。
原型鏈是用來實現繼承和屬性共享的有限對象鏈
這里提出了 繼承
的概念,如果你沒有 OOP 的概念,那恭喜你了,你能很快理解這種便捷的方式和概念,如果你是 Java/PHP 出身,你直接把這里的 繼承
理解為 重用吧。
JavaScript 引入繼承概念,就是為了實現屬性和方法的重用。
如果一個屬性或方法在自身中無法找到,那么會進入原型鏈查找這個屬性或方法,依次遍歷整個鏈,第一個被查找到的將會使用
如果沒有明確的指明原型對象的指向,那么原型對象的原型指針會指向 Object 的原型對象,而后者的原型對象指針指向 null,也就是原型鏈的重點。
舉個 ??,有三個對象,b,c 對 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);//?
這段代碼簡明扼要,通過 __proto__
代指原型鏈的鏈接過程,實際實現有所不同。由此構成了 a,b,c 之間的原型鏈如下圖(2)
上述例子中,重用了很多魔術變量,實際實現繼承的過程,我們對類的希望是抽象的 AST結構,擁有相同或相似的狀態結構,不同的狀態值和方法。于是,要引入構造函數對類進行初始化。
構造函數
由構造函數組成的類型,我們使用 new 關鍵字新建一個新的對象。用這個方式重寫上邊 abc 的例子,重用屬性和方法的時候使用構造函數+原型模式。
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 // ?
上邊代碼改進后,可以看到多了兩個關鍵詞,在 Foo 構造函數的原型對象中有一個 contstructor
屬性指回了構造函數本身。代碼邏輯圖如圖(3)所示
構造函數+原型對象 合在一起,被我們稱為 JavaScript 中的類。
ES6 對類的封裝過程進行了優化,引入 class
extends
super
等關鍵字,其實質還是基于原型鏈的委托繼承又叫原型繼承。
執行上下文
JavaScript 在 ES6之前是沒有塊級作用域的,基于這樣的人設,每段代碼都在自己的上下文環境中進行求值。此時函數作用域就被認為是局部作用域或者塊級作用域。
JavaScript 是通過棧結構來管理保存系統運行時的上下文狀態轉換的,稱為執行上下文棧。堆棧頂部的執行上下文稱為活動上下文。
觸發上下文堆棧中其他上下文的執行上下文稱作 caller
,被觸發的執行上下文稱為callee
,從函數角度更容易理解,執行函數叫做 caller,函數的容器稱為 callee。
一個局部作用域生效時,將其壓入執行上下文堆棧,作為活動執行上下文
執行上下文在 JavaScript 中也被實現為對象。對象中包含追蹤相關代碼執行過程的屬性。常見的幾個屬性有
- 變量對象
- 作用域鏈
- this 指針
變量對象
變量對象是一個抽象概念,變量對象中存儲了在當前上下文中存儲的變量和函數聲明。
函數表達式不包含在變量對象之中。
在全局執行上下文中,變量對象就是全局對象本身,考慮以下這個例子。
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
此時,全局變量對象的數據結構如圖(4)所示
其中函數表達式(閉包)并未進入全局變量對象,所以在全局調用也會失敗。
當一個變量對象進入執行時,稱作活動對象。
活動對象
活動對象是特殊的變量對象,也是實際存在的對象,在函數被觸發的時候創建,活動對象中默認包含 形參和arguments 對象。
函數表達式不在變量對象中,所以也不在活動對象中
函數在運行過程中,不僅可以使用活動對象中的屬性和方法,還可以使用父容器的屬性和方法。這種可以使用的實現原理就基于執行上下文的第二個屬性,作用域鏈。
作用域鏈
作用域鏈是查找變量值的鏈式結構,是一個對象列表。其實現原理與原型鏈相似。
如果一個變量在當前作用域中未找到定義,則遞歸上溯到父容器中查找,直到查找到作用域鏈的尾部。未找到返回
undefinded
如果函數引用了一個不是當前上下文中的標識符(變量,參數,方法)那么被引用的這個標識符被稱作自由變量,搜索自由變量的過程就是依次遍歷作用域鏈的過程。
看一個例子
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
console.log(x + y + z);
})();
})();
其中三個變量 x,y,z 在調用時發現,x,y 針對 bar 而講屬于自由變量,需要搜索作用域鏈獲取值。其搜索過程如圖(5)所示。
當使用 with/catch 語句時,將其中的語句插入作用域鏈的前端,使得被插入的片段既包含proto屬性又包含parent屬性,原型鏈的查找邏輯中優先查找proto屬性鏈。
活動變量在函數執行完畢后,將交付垃圾回收機制回收,如果不想被回收掉。那么就需要引入閉包的概念。
閉包
閉包是為了讓函數成為一等公民,解決函數作為參數和函數作為返回值時作用域鏈存活的問題。
當函數作為參數或返回值時,函數中自由變量訪問的容器函數尚可訪問(未被銷毀),該函數會在創建的時候,保存容器函數的作用域鏈。
閉包創建的函數作用域鏈 = 活動對象 + 父函數作用域鏈
保存是為了未來訪問的時候能夠訪問到。此時,父函數的作用域鏈被調用函數凍結了,稱此為靜態作用域。靜態作用域是一門語言能夠創造閉包的必需條件,JavaScript 就具備這個條件,現在給閉包下一個準確的定義:
閉包是一個方便查找自由變量的代碼塊,以塊級作用域為基礎構造靜態作用域,以保存父容器作用域鏈的集合體。
很抱歉,又說迷糊了。什么是閉包?JavaScript 中所有函數都是閉包。