天天看點

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

說明

圖解 Google V8 學習筆記

繼承是什麼?

簡單的說:繼承就是一個對象可以通路另外一個對象中的屬性和方法,在JavaScript 中,我們通過原型和原型鍊的方式來實作了繼承特性。。

不同的語言實作繼承的方式是不同的,其中最典型的兩種方式:

  • 基于類的設計:C++、Java、C#
  • 基于原型繼承的設計:JavaScript

原型繼承是如何實作的?

JavaScript 的每個對象都包含了一個隐藏屬性 ​

​__proto__​

​​,我們就把該隐藏屬性 ​

​__proto__​

​ 稱之為該對象的原型 (prototype),​

​__proto__​

​​ 指向了記憶體中的另外一個對象,我們就把 ​

​__proto__​

​ 指向的對象稱為該對象的原型對象。

例子:

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

我們讓 C 對象的原型指向 B 對象,讓 B 對象的原型指向 A 對象,那麼 C 對象就可以直接通路 B 以及 A 的方法跟屬性了。

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

當我們通過對象 C 來通路對象 A 中的 color 屬性時,V8 會先從對象 C 中查找,沒有查找到,接着繼續在 C 對象的原型對象 B 中查找,依舊沒有查找到,那麼繼續去對象 B 的原型對象 A 中查找,因為 color 在對象 A 中,那麼 V8 就傳回該屬性值。我們把這個查找屬性的路徑稱為原型鍊。

原型鍊 vs 作用域鍊

  • 原型鍊:是沿着對象的原型一級一級來查找屬性的
  • 作用域鍊:是沿着函數的作用域一級一級來查找變量的

實踐:利用 ​

​__proto__​

​ 實作繼承

下面先建立了兩個對象 animal 和 dog,如果讓 dog 對象繼承于 animal 對象,應該怎麼操作?

var animal = {
    type: "Default",
    color: "Default",
    getInfo: function () {
        return `Type is: ${this.type},color is ${this.color}.`
    }
}
var dog = {
    type: "Dog",
    color: "Black",
}      

最直接的方式就是通過設定 dog 對象中的 ​

​__proto__​

​ 屬性,将其指向 animal。

dog.__proto__ =      

使用 dog 來調用 animal 中的 getInfo 方法

dog.getInfo()      

輸出結果如下:

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

注意:通常隐藏屬性是不能使用 JavaScript 來直接與之互動的。雖然現代浏覽器都開了一個口子,讓 JavaScript 可以通路隐藏屬性 ​

​_proto_​

​​,但是在實際項目中,我們不應該直接通過 ​

​_proto_​

​ 來通路或者修改該屬性,應該使用構造函數來建立對象。

其主要原因有兩個:

  1. ​_proto_​

    ​ 是隐藏屬性,并不是标準定義的 ;
  2. 原型的實作做了很多複雜的優化,比如:通過隐藏類優化了很多原有的對象結構,是以通過直接修改​

    ​__proto__​

    ​ 會直接破壞現有已經優化的結構,造成嚴重的性能問題。

構造函數是怎麼建立對象的?

例子:

  1. 先建立一個 DogFactory 的函數,屬性通過參數進行傳遞,在函數體内,通過 this 設定屬性值。
function DogFactory(type,){
    this.type = type
    this.color = color
}      
  1. 再結合關鍵字​

    ​new​

    ​ 就可以建立對象(DogFactory 函數稱為構造函數)
var dog = new DogFactory('Dog', 'Black')      

V8 執行上面這段代碼時,做了什麼?

大緻分為三步:

  1. 建立了一個空白對象 dog
  2. 将 DogFactory 的 prototype 屬性設定為 dog 的原型對象
  3. 再使用 dog 來調用 DogFactory,這時候 DogFactory 函數中的 this 就指向了對象 dog,然後在 DogFactory 函數中,利用 this 對對象 dog 執行屬性填充操作

最終就建立了對象 dog。

模拟代碼如下:

var dog = {}  
dog.__proto__ = DogFactory.prototype
DogFactory.call(dog, 'Dog', 'Black')      

執行流程圖示意圖:

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

構造函數怎麼實作繼承?

例子:添加 constant_temperature 為 1 表示恒溫動物

function DogFactory(type,color){
    this.type = type
    this.color = color
    // 恒溫動物
    this.constant_temperature = 1
}
var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')      

dog1、dog2、dog3 占用空間示意圖:

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

可以看到 constant_temperature 屬性都占用了一塊空間,因為 dog 是恒溫動物,每個對象 沒必要為 constant_temperature 屬性都配置設定一塊空間,該屬性既然是通用的,可以設定屬性為公用的。

每個函數對象中都有一個公開的 prototype 屬性,當這個函數作為構造函數來建立一個新的對象時,新建立對象的原型對象就指向了該函數的 prototype 屬性。

圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

三個 dog 對象的原型對象都指向了 prototype,我們隻要讓 prototype 包含 constant_temperature 屬性,就能實作繼承了。

function DogFactory(type,color){
    this.type = type
    this.color = color
}
DogFactory. prototype.constant_temperature = 1

var dog1 = new DogFactory('Dog','Black')
var dog2 = new DogFactory('Dog','Black')
var dog3 = new DogFactory('Dog','Black')      
圖解 Google V8 # 06:原型鍊:V8是如何實作對象繼承的?

構造函數的​

​__proto__​

​ 和 prototype

  • 函數作為對象他得擁有一個​

    ​__proto__​

    ​,該屬性是隐藏屬性,并不是标準定義的 ;
  • 函數作為一個構造函數,它得擁有一個​

    ​prototype​

    ​,該屬性是标準定義的

上面的 ​

​DogFactory​

​​ 是 ​

​Function​

​​ 構造函數的一個執行個體,是以 ​

​DogFactory.__proto__ === Function.prototype​

​;

​DogFactory.prototype​

​​ 是調用 Object 構造函數的一個執行個體,是以 ​

​DogFactory.prototype.__proto__ === Object.prototype​

​;

是以 ​

​DogFactory._proto_​

​​ 和 ​

​DogFactory.prototype​

​ 沒有直接關系。

總結

繼續閱讀