從prototype與__proto__窺探JS繼承之源 | 掘金技術徵文

路易斯發表於2017-04-21

之前分享了一篇文章JS原型鏈與繼承別再被問倒了,發現繼承的問題研究到後面,一定會觸及到Object.prototype和Function.prototype這些概念,為了解答疑惑,本篇就抽絲剝繭,從prototype與__proto__來推出函式與物件的深層關係。
原文:詳解prototype與__proto__

概念

  1. prototype 是函式(function) 的一個屬性, 它指向函式的原型.
  2. __proto__ 是物件的內部屬性, 它指向構造器的原型, 物件依賴它進行原型鏈查詢,instanceof 也是依賴它來判斷是否繼承關係.

由上, prototype 只有函式才有, 其他(非函式)物件不具有該屬性. 而 __proto__ 是物件的內部屬性, 任何物件都擁有該屬性.

栗子

下面我們來吃個栗子幫助消化下:

function Person(name){
  this.name = name;
}
var p1 = new Person('louis');

console.log(Person.prototype);//Person原型 {constructor: Person(name),__proto__: Object}
console.log(p1.prototype);//undefined

console.log(Person.__proto__);//空函式, function(){}
console.log(p1.__proto__ == Person.prototype);//true複製程式碼

吃栗子時我們發現, Person.prototype(原型) 預設擁有兩個屬性:

  • constructor 屬性, 指向構造器, 即Person本身
  • __proto__ 屬性, 指向一個空的Object 物件

而p1作為非函式物件, 自然就沒有 prototype 屬性; 此處佐證了概念1

下面來看看__proto__屬性:

Person.__proto__ 屬性 指向的是一個空函式( function(){} ), 待會兒我們再來研究這個空函式.

p1.__proto__ 屬性 指向的是 構造器(Person) 的原型, 即 Person.prototype. 此處佐證了概念2

這裡我們發現: 原型鏈查詢時, 正是通過這個屬性(__proto__) 連結到構造器的原型, 從而實現查詢的層層深入.

概念1 不太理解的同學, 說明你們不會吃栗子, 我們們忽略他們. 對 概念2 不太理解的同學, 我們來多吃幾個栗子, 邊吃邊想:

var obj = {name: 'jack'},
    arr = [1,2,3],
    reg = /hello/g,
    date = new Date,
    err = new Error('exception');

console.log(obj.__proto__  === Object.prototype); // true
console.log(arr.__proto__  === Array.prototype);  // true
console.log(reg.__proto__  === RegExp.prototype); // true
console.log(date.__proto__ === Date.prototype);   // true
console.log(err.__proto__  === Error.prototype);  // true複製程式碼

可見, 以上通過 物件字面量 和 new + JS引擎內建構造器() 建立的物件, 無一例外, 它們的__proto__ 屬性全部指向構造器的原型(prototype). 充分佐證了 概念2 .

__proto__

剛才留下了一個問題: Person.__proto__ 指向的是一個空函式, 下面我們來看看這個空函式究竟是什麼.

console.log(Person.__proto__ === Function.prototype);//true複製程式碼

Person 是構造器也是函式(function), Person的__proto__ 屬性自然就指向 函式(function)的原型, 即 Function.prototype.

這說明了什麼呢?

我們由 "特殊" 聯想到 "通用" , 由Person構造器聯想一般的構造器.

這說明 所有的構造器都繼承於Function.prototype (此處我們只是由特殊總結出了普適規律, 並沒有給出證明, 請耐心看到後面) , 甚至包括根構造器Object及Function自身。所有構造器都繼承了Function.prototype的屬性及方法。如length、call、apply、bind(ES5)等. 如下:

console.log(Number.__proto__   === Function.prototype); // true
console.log(Boolean.__proto__  === Function.prototype); // true
console.log(String.__proto__   === Function.prototype); // true
console.log(Object.__proto__   === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Array.__proto__    === Function.prototype); // true
console.log(RegExp.__proto__   === Function.prototype); // true
console.log(Error.__proto__    === Function.prototype); // true
console.log(Date.__proto__     === Function.prototype); // true複製程式碼

JavaScript中有內建(build-in)構造器/物件共計13個(ES5中新加了JSON),這裡列舉了可訪問的9個構造器。剩下如Global不能直接訪問,Arguments僅在函式呼叫時由JS引擎建立,Math,JSON是以物件形式存在的,無需new。由於任何物件都擁有 __proto__ 屬性指向構造器的原型. 即它們的 __proto__ 指向Object物件的原型(Object.prototype)。如下所示:

console.log(Math.__proto__ === Object.prototype);  // true
console.log(JSON.__proto__ === Object.prototype);  // true複製程式碼

如上所述, 既然所有的構造器都來自於Function.prototype, 那麼Function.prototype 到底是什麼呢?

Function.prototype

我們借用 typeof 運算子來看看它的型別.

console.log(typeof Function.prototype) // "function"複製程式碼

實際上, Function.prototype也是唯一一個typeof XXX.prototype為 “function”的prototype。其它的構造器的prototype都是一個物件。如下:

console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.prototype)   // object
console.log(typeof Object.prototype)   // object
console.log(typeof Array.prototype)    // object
console.log(typeof RegExp.prototype)   // object
console.log(typeof Error.prototype)    // object
console.log(typeof Date.prototype)     // object複製程式碼

JS中函式是一等公民

既然Function.prototype 的型別是函式, 那麼它會擁有 __proto__ 屬性嗎, Function.prototype.__proto__ 會指向哪裡呢? 會指向物件的原型嗎? 請看下方:

console.log(Function.prototype.__proto__ === Object.prototype) // true複製程式碼

透過上方程式碼, 且我們瞭解到: Function.prototype 的型別是函式, 也就意味著一個函式擁有 __proto__ 屬性, 並且該屬性指向了物件(Object)構造器的原型. 這意味著啥?

根據我們在 概念2 中瞭解到的: __proto__ 是物件的內部屬性, 它指向構造器的原型.

這意味著 Function.prototype 函式 擁有了一個物件的內部屬性, 並且該屬性還恰好指向物件構造器的原型. 它是一個物件嗎? 是的, 它一定是物件. 它必須是.

實際上, JavaScript的世界觀裡, 函式也是物件, 函式是一等公民.

這說明所有的構造器既是函式也是一個普通JS物件,可以給構造器新增/刪除屬性等。同時它也繼承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。

Object.prototype

函式的 __proto__ 屬性指向 Function.prototype, 如: Person.__proto__ —> Function.prototype

Function.prototype 函式的 __proto__ 屬性指向 Object.prototype, 如: Function.prototype.__proto__ —> Object.prototype.

那麼Object.prototype.__proto__ 指向什麼呢?

console.log(Object.prototype.__proto__ === null);//true複製程式碼

由於已經到頂, JS世界的源頭一片荒蕪, 竟然什麼都沒有! 令人嗟嘆不已.

都說一圖勝千言, 我也不能免俗. 下面附一張 stackoverflow 上的圖:

從prototype與__proto__窺探JS繼承之源 | 掘金技術徵文

這張圖也指出:

  • Object.__proto__ == Function.prototype,
  • Function.__proto__ == Function.prototype.
//雖然上面做過測試, 我們還是再次測試下
console.log(Object.__proto__   == Function.prototype);//true
console.log(Function.__proto__ == Function.prototype);//true複製程式碼

由於物件構造器 (Object) 也是構造器, 又構造器都是函式, 又函式是一等公民, 函式也是物件.

故, 物件構造器 (Object) 擁有3種身份:

  • 構造器
  • 函式
  • 物件

推而廣之, 所有構造器都擁有上述3種身份.

由於構造器是 物件 (身份3), 理所當然擁有 __proto__ 屬性, 且該屬性一定指向其構造器的原型, 也就是指向 函式 (身份2) 構造器(Function)的原型, 即 Function.prototype. 於是我們證明了上面那句 所有的構造器都繼承於Function.prototype (身份1).

注: 上面程式碼中用到的 __proto__ 目前在IE6/7/8/9中並不支援。IE9中可以使用Object.getPrototypeOf(ES5)獲取物件的內部原型。


附上網友的疑問(問題提得特別好,問出了函式與物件最尖銳的歸宿問題):

問題背景:先有 Object.prototype(原型鏈頂端),Function.prototype 繼承 Object.prototype 而產生,最後,Function 和 Object 和其它建構函式繼承 Function.prototype 而產生。

以下是具體問題:

  1. 先有 Object.prototype,再有 Object,那麼先有 Object.prototype 裡面的這個 Object 代表的是什麼呢?

  2. Function.__proto__=== Function.prototype;
    Function 建構函式的 prototype 屬性和__proto__屬性都指向同一個原型,是否可以說 Function 物件是由 Function 建構函式建立的一個例項?

  3. Object instanceof Function // true
    Function instanceof Object // true
    Object 本身是建構函式,繼承了 Function.prototype; Function 也是物件,繼承了Object.prototype。感覺成了雞和蛋的問題了。

  4. 比如說:
    function Person(){}
    Person.prototype 是一個物件,為什麼 Function.prototype 卻是一個函式呢,當然函式也是一個物件,為什麼要這麼設計呢?

這裡是疑惑:

感覺有些地方很難理解,總感覺有種悖論一樣,還有人扯到羅素悖論,各有說辭,不知道樓主怎麼看。。。

通讀全文如果你能回答這些問題,說明看懂了本文。否則請允許我建議你再讀一篇,別打我。


---華麗麗的分割線---

以下是回答:
答1: 先有的一定是Object,它是BOM物件,列印出來為:function Object() { [native code] },然後才是Object.prototype。這不矛盾,請看分析。
首先:Object.create(null)你應該知道,它可以建立一個沒有原型的物件。如下:
Object.prototype.__proto__ == Object.create(null).__proto__
結果為true,也就是說,Object.prototype是使用這種方式生成的,然後才綁在Object的prototype屬性上。為什麼這裡沒有用===,因為__proto__指向的是物件,物件是沒法比較相等的。
要知道,物件可以先生成,然後再賦予新的屬性。

答2: Function.__proto__=== Function.prototype 這沒毛病。
我們都知道,Function是一個函式,只要是函式,它的__proto__就指向 Funtion.prototype. 這是繼承的設定。那麼我們怎麼理解這種構造器本身就繼承本身的prototype屬性的現象呢?
Function生成時,是沒有__proto__屬性的,它是一個BOM物件,列印出來為:function Function() { [native code] },Function.prototype同樣是BOM物件,列印出來為:function () { [native code] },那麼可以這麼理解:
Function的__proto__和prototype屬性都是後面才指向同一個BOM物件的。

答3: 這不是蛋和雞的問題,計算機中沒有蛋和雞。
Object instanceof Function // true 。Object是構造器函式,你必須認,有new Object()為證。
答2已經給了結論:只要是函式,它的__proto__就指向 Funtion.prototype。Object也是函式。因此它的__proto__屬性會指向Funtion.prototype。故而Object instanceof Function 為 true。

答4: Function.prototype是一個函式,也是物件。這是自然的。
那麼為什麼這麼設計呢?你注意到沒有 typeof Function.prototype === "function" 結果為true。注意:只有Function的prototype是函式,其他都是普通物件。它是一個橋樑,我們一直說函式也是物件,只有這個滿足了, 函式才能是物件,關於『js中函式是一等公民』的定理,Function.prototype同時是函式, 又是原型物件便是佐證。

說了說去,都回到了Function.prototype、Function和Object的問題上,本質上它們都是BOM物件,即它們都在瀏覽器global物件(這個js是訪問不到的)生成之前生成的,後面的指向關係,可以認為是js世界繼承規律的一種設定,有了這個設定,js遊戲才能玩下去。


本文就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.

本文作者: louis

本文連結: louiszhai.github.io/2015/12/17/…

本次活動連結:juejin.im/post/58d8e9…

參考文章

相關文章