說說JS中的原型物件和原型鏈

小牛的後會發表於2019-05-04

理解原型物件(有些文章簡稱為原型)和原型鏈,是理解JS的重要一環。下面是筆者對JS中原型的理解,

函式物件

俗話說,JS中萬物皆物件。函式也是一個物件,只不過函式是在特定環境中執行程式碼的物件。

什麼是函式物件?每宣告一個函式,此函式在JS執行解釋時都會被當作一個物件來維護,這就是函式物件。JS中宣告函式的方式有:

function fn1(){}
var fn2 = function(){}
var fn3 = new Function()
複製程式碼

所以可以理解為fn1、fn2、fn3都是函式物件。JS中還包括一些系統內建的函式物件,比如:

Function  Object  Array  String  Number  RegExp
複製程式碼

函式物件之外的物件都是普通物件。函式物件能建立普通的物件,反之則不行。

理解原型物件(其實就一普通物件)

1、只有函式物件才擁有原型物件

也即無論什麼時候以什麼方式建立一個函式(函式物件),都會根據特定的規則為該函式建立一個prototype屬性(原型物件的地址的引用),這個屬性就是指向該函式的原型物件。比如:

function Person () {};
console.log(Person.prototype) // Person.prototype就是Person的原型物件,實際是原型物件的記憶體地址的引用
複製程式碼

說說JS中的原型物件和原型鏈
看到沒有,原型物件並不神祕,就是一個普通的物件,只不過其預設有了constructor__proto__(下一節會講)屬性而已(其中__proto__不建議在實際中應用,因為在有些瀏覽器可能並沒有實現該屬性)。

由上圖看出,函式Person的原型物件(Person.prototype)預設擁有一個屬性constructor,此屬性就是用來重新指向函式Person

function Person () {};
Person.prototype.constructor === Person // true
複製程式碼

說說JS中的原型物件和原型鏈

2、普通物件與原型物件的關係

一般我們定義一個建構函式(建構函式其實就是普通的函式,只不過目的是建立物件),然後通過new操作符來建立一個普通物件。

function Person (name) {
    this.name = name;
    this.age = 18;
}
var xiaoming = new Person('小明'); // {name: '小明', age: 18}
var xiaohong = new Person('小紅'); // {name: '小紅', age: 18}
複製程式碼

在上述程式碼中,變數xiaomingxiaohong是建構函式Person的例項。我們通過上一節知道了Person與其原型物件的關係,但例項與建構函式的原型物件有什麼關係呢?

每當呼叫建構函式建立一個例項即普通物件後,該例項將包含一個內部的指標[[Prototype]],這個指標指向的就是建構函式的原型物件。

目前ECMAScript的標準中並沒有實現標準的訪問該指標的方式,但像Firefox、Chrome和Safari等瀏覽器實現了__proto__屬性,此屬性就是用來訪問指標[[Prototype]],所以可以借用__proto__屬性展示例項和原型物件的關係。

xiaoming.__proto__ === Person.prototype // true
xiaohong.__proto__ === Person.prototype // true
複製程式碼

3、總結上述兩小節

每建立一個函式,就會為相應的函式建立一個prototype的屬性,這個屬性指向了函式的原型物件,這個函式的原型物件會預設擁有一個constructor屬性,此屬性指向了對應的函式。而使用new操作符呼叫函式建立出來的例項,會擁有一個內部的指標[[Prototype]],此指標指向函式的原型物件。

千言萬語不如一幅圖:

說說JS中的原型物件和原型鏈

原型鏈

由上節我們可以知道,原型物件上的屬性和方法被所有例項所共享的。每當訪問一個物件的屬性或者方法時,會首先搜尋物件自身,如果找到了此屬性或者方法,則直接返回,否則向對應的原型物件上面搜尋,如果找到則直接返回,否則繼續向原型物件的原型物件上查詢,直到搜尋到null,丟擲錯誤或返回undefined

function Person (name) {
    this.name = name;
    this.age = 18;
}
Person.prototype.sayName () { // 在Person的原型物件上新增的方法,被所有例項共享
    console.log(this.name);
}
var xiaoming = new Person('小明'); // {name: '小明', age: 18}
xiaoming.sayName(); // 小明
複製程式碼

上面程式碼中,例項xiaoming本身並沒有sayName方法,但卻成功呼叫了。 其實就是通過例項內部的[[Prototype]]指標去原型物件Person.prototype 上找對應的方法,然後呼叫。

如果我呼叫一個例項本身和原型物件都沒有的方法,其過程是怎麼樣的呢?

xiaoming.sayAge() // 例項本身和原型物件都不存在的方法
複製程式碼

(1)首先搜尋xiaoming這個物件,並沒有sayAge方法,

說說JS中的原型物件和原型鏈
(2)繼續向原型物件搜尋(通過內部的[[Prototype]]指標)。沒有找到sayAge方法
說說JS中的原型物件和原型鏈
(3)繼續向原型物件的原型物件上搜尋,即xiaoming.__proto__.__proto__。也沒有找到sayAge方法。
說說JS中的原型物件和原型鏈
(4)繼續向原型物件的原型物件的原型物件上搜尋,即xiaoming.__proto__.__proto__.__proto__,但發現xiaoming.__proto__.__proto__.__proto__null,停止搜尋,丟擲錯誤或返回undefined

如果原型物件和例項上具有同名的屬性或方法,則搜尋時取最近的。

如上述的原型鏈的搜尋機制,你通過閱讀本文知道xiaoming.__proto__Person.prototype,但xiaoming.__proto__.__proto__呢? 不說話看圖:

說說JS中的原型物件和原型鏈
由此,可得到下面的關係圖:
說說JS中的原型物件和原型鏈

思考

原型鏈中的關係圖其實還缺少一環,就是內建函式FunctionFunction比較特殊,有興趣的可以去研究下FunctionObject的關係。

本文是筆者對原型物件和原型鏈的理解,如有錯誤或不足的地方,歡迎指正。

相關文章