這是一篇關於JavaScript原型知識的還債帖

前言

本人從事前端開發的工做三年有餘,我要向你坦白,時至今日我對JS原型仍然是隻知其一;不知其二,當年的校招面試關於JS原型都是「臨時抱佛腳」,死記硬背混過去鳥~ ~。html

在往後工做中,我已熟練的使用Function去封裝類,使用mixin去豐富類,使用new去實例化我鐘意的對象(單身狗的悲哀),然而卻忘了它們背後蘊含的原理。前端

痛定思痛,本文算做是還債帖。面試

兩種數據類型

在JS裏,分了兩種數據類型,分別爲基本數據類型引用數據類型算法

目前基本數據類型已有:number string boolean undefined null symbol bigint瀏覽器

它們的區分點在存儲位置的不一樣。基本數據類型是直接存儲在棧內存中的,而引用數據類型則存儲在堆內存裏的。markdown

《天天都在寫的JS判斷語句,你真的瞭解嗎?》一文中提到咱們可使用typeof運算符去識別變量的數據類型。以下圖:app

在其中,typeof null === 'object'是個例外,它是惟一一個沒法經過typeof識別出來的基本數據類型。函數

計算機都是以二進制方式來存儲數據,JS中若是二進制前三位都是0時會斷定爲object類型,而null的二進制前三位剛好都是0,因此返回objectoop

除了null這個特例,咱們發現全部引用數據類型又分爲了兩個陣營:functionobject。它們是否有內在聯繫呢?post

Function和Object是好基友👬

先拋出個問題,什麼是對象? 我會給出這樣的答案:所謂對象,都有本身的屬性和方法

這是我本身的認識,不必定正確,歡迎你評論,說出你的想法。

那麼,函數是對象嗎?是!由於它符合上面的定義:

function Foo(){}
Foo.name = 'Foo'
Foo.getName = function(){return Foo.name}
Foo.getName()  // Foo
複製代碼

在JS中,對象是「第一等公民」,那麼怎樣纔算「第一等公民」?

  • 能夠被動態建立;
  • 能夠賦值給變量;
  • 能夠做爲函數的入參或出參;
  • 能夠包含本身的屬性或方法;

顯然,函數符合這四個條件,因此函數也是「第一等公民」。因此,函數即對象

事實上,在ECMA標準中,已直接將函數劃歸到 Sandard Built-in ECMAScript Objects

既然「函數即對象」,JS又怎樣具體區分出函數與對象呢?ECMA給出了答案: 若是一個對象有內部屬性[[Call]],則它是一個函數; 若是一個對象沒有內部屬性[[Call]],則它是一個普通對象;

函數肩負了兩項職責:

  1. 邏輯函數:用於封裝業務邏輯,處理事務;
  2. 構造函數:用於對象實例化,此時必須用new操做符調用函數;
function Person(name){
    this.name = name
}
let p = new Person()
typeof Person  // function
typeof p  // object
複製代碼

在JS中,已經爲咱們準備好不少的內置構造函數,好比Function Object Array Date RegExp等。當你使用typeof操做符識別它們,它們都會返回function

typeof Function  // function
typeof Array  // function
複製代碼

你有沒有發現,new Date()Date() 你均可以獲得一個 Date 實例化對象,你知道怎麼作到的嗎?歡迎評論 。

至此咱們有了基本認知,首先函數擁有對象相同的能力(函數即對象),同時函數還能實例化對象。在Function和Object背後必定有着「隱蔽」的內在聯繫。

原型對象prototype

計算機執行任何邏輯都須要成本:時間成本和內存成本。一樣一件事兩種作法,顯然咱們會選擇成本更低的那種作法。一樣的,JS也「不笨」。

先看以下示例代碼:

let dog1 = {
    name: '旺財',
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}

let dog2 = {
    name: '大黃',
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
} 
複製代碼

狗生艱難,都要揹負屬性species和方法bark,任重而道遠。因而旺財和大黃商議,能不能有個」代理「的機制,將共性部分的屬性都交給這個代理,由代理全權負責,本身只保留不一樣的部分(name)。因而有了這個」代理「:

let agent = {
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}
let dog1 = {
    name: '旺財',
    __agent__: agent
}
let dog2 = {
    name: '大黃',
    __agent__: agent
}
複製代碼

當訪問到一個屬性,本身身上沒有的,就去」代理「那裏找。若是找到了,皆大歡喜;未果,萬分抱歉。

」代理「就如同製做陶瓷碗的模具,它記錄了陶瓷碗的高度、碗口大小等,這樣保證製做出每一個碗都是同種規格。而各個碗又能夠塗上不一樣的色彩,保留了它的個性。

在JS中,這個」代理「就是原型prototype。原型保存了同類型對象的共享屬性和方法,實質上就是爲了節省內存空間。從上面的示例中看到agent是一個對象,一樣JS中prototype也是一個對象,即原型對象

那麼,這個prototype對象應該放在哪裏最合適呢?(小蝌蚪開始找媽媽了)

咱們從上一節中知道,對象能夠由函數實例化,即用new操做符運行函數,至關於同類型的對象也都是由一樣的函數實例化的,此時函數變身爲構造函數。-->(同規格的碗必定是從一個模子裏倒出來的)

另外一方面,函數即對象,那麼就能夠在函數上掛載一些屬性。Bingo!把prototype掛載在構造函數上必定是最合適的,由於對象實例化必定會經歷構造函數運行。 因而,修改一下上面的示例:

function Dog(name){
    this.name = name
}
Dog.prototype = {
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}
// Oops, 好像丟了什麼關鍵信息?

let dog1 = new Dog('旺財')
let dog2 = new Dog('大黃')
dog1.species === dog2.species  // true
複製代碼

細心的讀者在這裏應該會糾正我了,給Dog構造函數直接賦值prototype會丟失一些信息。哈,沒錯,就是contructor屬性,暫且不表。先要研究在new的過程當中,是怎樣操做這個原型的。

new操做符 與 [[Prototype]]

引自MDN中的定義,new運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。

管它是啥,咱們關心的是new內部的貓膩。 以let dog = new Dog('旺財')爲例,new進行了以下操做:

  • Step 1:建立一個空對象 obj = {}
  • Step 2:將obj的內部屬性[[Prototype]]指向構造函數Dog的原型,即obj.[[Prototype]] = Dog.prototype
  • Step 3:將構造函數Dogthis指向這個空對象obj
  • Step 4:執行構造函數Dog,函數內部對this的操做等同於操做obj對象;
  • Step 5:若是函數Dog指定了返回值,則正常返回這個返回值;不然,返回obj對象;

以上就是new操做符的黑魔法,它已經幫咱們完成了對象實例化後原型的綁定,完成了「代理」指定。 其中,提到了obj空對象的內部屬性[[Prototype]],它在new過程當中會被指向構造函數的原型,至關於這個空對象obj繼承了原型對象。 依據ECMA標準中的描述,每一個對象都會有這個[[Prototype]]內部屬性,爲的就是實現對象繼承。

既然是內部屬性,咱們天然沒法直接經過obj.[[Prototype]]訪問到。目前有兩個方式能夠從一個對象obj上獲取到它繼承的原型對象:

  1. 野路子obj.__proto__
  2. ECMA官方Object.getPrototypeOf(obj)

之因此將obj.__proto__稱爲野路子是由於對象上的__proto__屬性是由各個瀏覽器本身實現的,目的是爲了方便開發者調試代碼,ECMA官方可不認可這種方式哦。

在ECMA2015中沒有任何關於__proto__的說明,在最新的ECMA標準中你能夠在附錄中找到關於__proto__說明,但它是可配置的屬性,便可能會被任意覆蓋。

所以切記:不要在生產環境中使用__proto__,請使用Object.getPrototypeOf(obj),當且僅在開發環境中可使用__proto__。 若是你實在忍不住,也請作好字段校驗 const hasProto = '__proto__' in {}

prototype vs [[Prototype]]
這裏勢必要把二者拿出來比較一下,畢竟用了同一個單詞嘛。請記住如下兩句話:

  • 只有函數纔有prototype屬性,prototype是一個對象(即,原型對象),對象上包含全部實例對象共享的屬性或方法;
  • 全部對象都有[[Prototype]]內部屬性,它指向建立該對象的構造函數的原型對象;

比較拗口,請多讀幾遍這兩句話,理解其中含義!

依據「函數即對象」的定義,函數會同時擁有prototype[[Prototype]]屬性,對象不會有prototype屬性,只有[[Prototype]]屬性。

至此,函數與對象果真是有「一腿」的,依靠prototype[[Prototype]]產生了「剪不斷理還亂」的關係。

constructor屬性

每一個對象都有構造函數!

上面提到咱們能夠經過Object.getPrototyoeOf(obj)方法方便的獲取到任一對象的原型對象。那麼咱們也須要方便的從一個對象上面輕鬆獲取到該對象的構造函數。

讀取對象obj.constructor屬性就能夠得到它的構造函數,這裏須要注意的是,constructor是掛載在構造函數的原型對象prototype上的。而在實例化對象上讀取的constructor屬性都是從它的構造函數原型上繼承來的。

上節中咱們漏掉了對constructor的修正,若是粗暴的直接修改Dog函數的原型,會丟失constructor屬性,訪問dog1.constructor,獲得的是從Object構造函數原型上繼承來的constructor,顯然是不正確的,須要將constructor指向正確的Dog函數上:

function Dog(name){
    this.name = name
}
Dog.prototype = {
    constructor: Dog,
    species: '犬',
    bark: function(){
        console.log('汪汪!')
    }
}

let dog1 = new Dog('旺財')
dog1.constructor === Dog  // true
複製代碼

有個面試題:問Dog.prototype = {xxx: yyy}Dog.prototype.xxx = yyy的區別在哪? --> 前者須要修正constructor

真的須要修正constructor嗎?
平心而論,我在寫函數封裝類的時候,常常會覆蓋它的原型,而且忘記修正constructor屬性,但也沒有任何異常的事情發生,長此以往,不修正constructor屬性成了天然。

沒錯,不會有什麼異常的事情發生,前提是你不會顯式的調用constructor

constructor指向構造函數,因此constructor是函數,能夠被直接調用。 仍然是上面的示例(不修正constructor),一般而言,咱們是直接調用new Dog()去實例化對象,而不會直接調用constructor,那若是直接調用constructor呢?

let dog2 = new dog1.constructor('大黃')
dog2.name  // undefined
dog2 instanceof Dog // false
dog2 instanceof Object // true
dog2 instanceof String // true
複製代碼

爲何dog2String的實例,留給你去思考啦

若是不顯式的調用constructor好像也沒啥問題啊,那你能保證你使用的某個第三方庫就不會顯式的調用你傳遞給它的對象的constructor嗎?

養成習慣,及時修正你的constructor

再談Function 和 Object

從上節中,大體清楚瞭如何經過new構造函數來實例化一個對象,那麼做爲普通函數和普通對象的構造函數FunctionObject是怎樣的關係?

首先,FunctionObject是內置構造函數,因此它們是函數,毋庸置疑:

typeof Function // function
typeof Object // function
複製代碼

其次既然是函數,那麼它們都有prototype對象屬性,即Function.prototype Object.prototype。 而後,prototype是對象,就有[[Prototype]]內部屬性,指向建立該對象的構造函數的原型對象:

Function.prototype.__proto__ === Object.prototype  // true
Object.prototype.__proto__ === Object.prototype // Oops!糟了,若是這裏要是成立的話,就會陷入死循環了
複製代碼

JavaScript爲了避免陷入死循環,所謂「恩恩怨怨什麼時候了」,總歸須要一個終點,不能無休止遍歷下去,所以當訪問到Object.prototype.__proto__時,將強制返回null,意思是你訪問到頭了,沒有更多內容了,洗洗睡吧。(標準中關於Object Prototype的說明

同時,咱們在標準中也獲取到以下兩條信息:

  1. 每一個內置函數或內置構造函數的[[Prototype]]都指向Function.prototype
  2. 每一個內置原型對象的[[Prototype]]都指向Object.prototype,除了Object.prototype自身之外;

在前面,咱們知道函數的原型應該是對象,即typeof Foo.prototype === 'object'是成立的。可是!Function.prototype倒是一個例外typeof Function.prototype === 'function',不是object

我從ECMA2015中找到了關於Function.prototype的描述:

The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined. 即 Function.prototype是函數對象 (它的內部屬性[[Class]] 是 ‘Function’)

自ECMA2015如此描述後,爲了兼容它,之後的全部ECMA版本也都「將錯就錯」了。

ECMA:你大爺終究是你大爺,哈哈

至此,能夠獲得Function與Object的關係圖以下:

原型鏈

討論了這麼多,思緒有點亂了,我將全部關係者整合到一張關係圖中:

從這張關係圖中,咱們能夠概括出幾個信息點:

  1. 函數必然有prototype屬性,原型是一個對象,包含全部實例對象共享的屬性或方法;
  2. 不管函數仍是對象都有[[Prototype]]內部屬性,指向原型對象,其中Object.prototype.[[Prototype]]指向null,以表示原型查找的終點;
  3. prototype對象上掛載了constructor屬性,指向構造函數;

那麼當咱們在談論原型鏈的時候,究竟咱們在談論什麼?

若是你想真實看到這條「鏈」,那麼它就是關係圖中的粉色虛線框所示,可是這裏原型鏈只是賓語,談論原型鏈時,不能夠丟失主語,即foo對象的原型鏈。直觀的說,原型鏈就是對象間用[[Prototype]]內部屬性串聯起來的單向鏈表

因此,函數Foo的原型鏈是怎樣的,我想你也能在關係圖找到。 函數Foo的原型鏈能夠訪問到Function.prototype.callFunction.prototype.apply

在JS中,我理解的原型鏈實際上是一種算法,即在對象上查找屬性的算法。 以運行foo.someFunc()爲例:

  • 首先,查找foo對象上是否已定義someFunc方法,未果,next;
  • 取得foo.[[Prototype]],即Foo.prototype對象,查找對象上是否已定義someFunc方法,未果,next;
  • 取得Foo.prototype.[[Prototype]],即Object.prototype對象,查找對象上是否已定義someFunc方法,未果,next;
  • 取得Object.prototype.[[Prototype]],發現是null,宣告這次查找失敗,拋出TypeError錯誤,結束這次查找,Game over。

總結

碼了這麼多字,無非想和你達成以下共識:

  • 函數即對象;
  • 只有函數纔有prototype屬性,prototype是一個對象(即,原型對象),對象上包含全部實例對象共享的屬性或方法;
  • 全部對象都有[[Prototype]]內部屬性,它指向建立該對象的構造函數的原型對象prototype
  • 通常而言,原型對象上都會有constructor屬性,指向原型綁定的構造函數;
  • 原型鏈是一種在對象上查找屬性的算法;

你是否有不一樣的見解?歡迎評論。

預告
基於原型,JS是如何作到類繼承的?ES6中,class A extends B背後藏着什麼貓膩?如何經過「混入mixin」來完成多重繼承?

最後

碼字不易,若是:

  • 這篇文章對你有用,請不要吝嗇你的小手爲我點贊;
  • 有不懂或者不正確的地方,請評論,我會積極回覆或勘誤;
  • 指望與我一同持續學習前端技術知識,請關注我吧;
  • 轉載請註明出處;

您的支持與關注,是我持續創做的最大動力!

相關文章
相關標籤/搜索