深度解析原型中的各個難點

yck發表於2018-03-14

本文同步分佈在我的 Github

本文不會過多介紹基礎知識,而是把重點放在原型的各個難點上。

深度解析原型中的各個難點

大家可以先仔細分析下該圖,然後讓我們進入主題

prototype

首先來介紹下 prototype 屬性。這是一個顯式原型屬性,只有函式才擁有該屬性。基本上所有函式都有這個屬性,但是也有一個例外

let fun = Function.prototype.bind()
複製程式碼

如果你以上述方法建立一個函式,那麼可以發現這個函式是不具有 prototype 屬性的。

prototype 如何產生的

當我們宣告一個函式時,這個屬性就被自動建立了。

function Foo() {}
複製程式碼

並且這個屬性的值是一個物件(也就是原型),只有一個屬性 constructor

深度解析原型中的各個難點

constructor 對應著建構函式,也就是 Foo

constructor

constructor 是一個公有且不可列舉的屬性。一旦我們改變了函式的 prototype ,那麼新物件就沒有這個屬性了(當然可以通過原型鏈取到 constructor)。

深度解析原型中的各個難點

那麼你肯定也有一個疑問,這個屬性到底有什麼用呢?其實這個屬性可以說是一個歷史遺留問題,在大部分情況下是沒用的,在我的理解裡,我認為他有兩個作用:

  • 讓例項物件知道是什麼函式構造了它
  • 如果想給某些類庫中的建構函式增加一些自定義的方法,就可以通過 xx.constructor.method 來擴充套件

_proto_

這是每個物件都有的隱式原型屬性,指向了建立該物件的建構函式的原型。其實這個屬性指向了 [[prototype]],但是 [[prototype]] 是內部屬性,我們並不能訪問到,所以使用 _proto_ 來訪問。

因為在 JS 中是沒有類的概念的,為了實現類似繼承的方式,通過 _proto_ 將物件和原型聯絡起來組成原型鏈,得以讓物件可以訪問到不屬於自己的屬性。

例項物件的 _proto_ 如何產生的

從上圖可知,當我們使用 new 操作符時,生成的例項物件擁有了 _proto_屬性。

function Foo() {}
// 這個函式是 Function 的例項物件
// function 就是一個語法糖
// 內部呼叫了 new Function(...)
複製程式碼

所以可以說,在 new 的過程中,新物件被新增了 _proto_ 並且連結到建構函式的原型上。

new 的過程

  1. 新生成了一個物件
  2. 連結到原型
  3. 繫結 this
  4. 返回新物件

在呼叫 new 的過程中會發生以上四件事情,我們也可以試著來自己實現一個 new

function create() {
    // 建立一個空的物件
    let obj = new Object()
    // 獲得建構函式
    let Con = [].shift.call(arguments)
    // 連結到原型
	obj.__proto__ = Con.prototype
    // 繫結 this,執行建構函式
    let result = Con.apply(obj, arguments)
    // 確保 new 出來的是個物件
    return typeof result === 'object' ? result : obj
}
複製程式碼

對於例項物件來說,都是通過 new 產生的,無論是 function Foo() 還是 let a = { b : 1 }

對於建立一個物件來說,更推薦使用字面量的方式建立物件。因為你使用 new Object() 的方式建立物件需要通過作用域鏈一層層找到 Object,但是你使用字面量的方式就沒這個問題。

function Foo() {}
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 }
// 這個字面量內部也是使用了 new Object()
複製程式碼

Function.proto === Function.prototype

對於物件來說,xx.__proto__.contrcutor 是該物件的建構函式,但是在圖中我們可以發現 Function.__proto__ === Function.prototype,難道這代表著 Function 自己產生了自己?

答案肯定是否認的,要說明這個問題我們先從 Object 說起。

從圖中我們可以發現,所有物件都可以通過原型鏈最終找到 Object.prototype ,雖然 Object.prototype 也是一個物件,但是這個物件卻不是 Object 創造的,而是引擎自己建立了 Object.prototype所以可以這樣說,所有例項都是物件,但是物件不一定都是例項。

接下來我們來看 Function.prototype 這個特殊的物件,如果你在瀏覽器將這個物件列印出來,會發現這個物件其實是一個函式。

深度解析原型中的各個難點

我們知道函式都是通過 new Function() 生成的,難道 Function.prototype 也是通過 new Function() 產生的嗎?答案也是否定的,這個函式也是引擎自己建立的。首先引擎建立了 Object.prototype ,然後建立了 Function.prototype ,並且通過 __proto__ 將兩者聯絡了起來。這裡也很好的解釋了上面的一個問題,為什麼 let fun = Function.prototype.bind() 沒有 prototype 屬性。因為 Function.prototype 是引擎建立出來的物件,引擎認為不需要給這個物件新增 prototype 屬性。

所以我們又可以得出一個結論,不是所有函式都是 new Function() 產生的。

有了 Function.prototype 以後才有了 function Function() ,然後其他的建構函式都是 function Function() 生成的。

現在可以來解釋 Function.__proto__ === Function.prototype 這個問題了。因為先有的 Function.prototype 以後才有的 function Function() ,所以也就不存在雞生蛋蛋生雞的悖論問題了。對於為什麼 Function.__proto__ 會等於 Function.prototype ,個人的理解是:其他所有的建構函式都可以通過原型鏈找到 Function.prototype ,並且 function Function() 本質也是一個函式,為了不產生混亂就將 function Function()__proto__ 聯絡到了 Function.prototype 上。

總結

  • Object 是所有物件的爸爸,所有物件都可以通過 __proto__ 找到它
  • Function 是所有函式的爸爸,所有函式都可以通過 __proto__ 找到它
  • Function.prototypeObject.prototype 是兩個特殊的物件,他們由引擎來建立
  • 除了以上兩個特殊物件,其他物件都是通過構造器 new 出來的
  • 函式的 prototype 是一個物件,也就是原型
  • 物件的 __proto__ 指向原型, __proto__ 將物件和原型連線起來組成了原型鏈

深度解析原型中的各個難點

相關文章