JavaScript 核心

一門語言,無論是機器語言,還是人類語言。在學習的時候,我們從入門到精通會在以下幾個角度做分別的深入:

  • 詞法
  • 語法
  • 句法

對于 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 中所有函數都是閉包。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 作者:Dmitry A. Soshnikov編譯地址:http://dmitrysoshnikov.com/ecm...
    IT程序獅閱讀 3,356評論 2 12
  • 原文:http://dmitrysoshnikov.com/ecmascript/javascript-the-c...
    jaysoul閱讀 484評論 0 0
  • 繼承 一、混入式繼承 二、原型繼承 利用原型中的成員可以被和其相關的對象共享這一特性,可以實現繼承,這種實現繼承的...
    magic_pill閱讀 1,076評論 0 3
  • 1,javascript 基礎知識 Array對象 Array對象屬性 Arrray對象方法 Date對象 Dat...
    Yuann閱讀 945評論 0 1
  • ①為了應對自己所學的專業,自己有點欲望買個《本草綱目》看看,,誰叫我學生物工程嘞,,想讓自己專業有所發展就需自己多...
    雷帥帥閱讀 150評論 0 0