本人從事前端開發的工做三年有餘,我要向你坦白,時至今日我對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,因此返回object
oop
除了null
這個特例,咱們發現全部引用數據類型又分爲了兩個陣營:function
和 object
。它們是否有內在聯繫呢?post
先拋出個問題,什麼是對象? 我會給出這樣的答案:所謂對象,都有本身的屬性和方法
這是我本身的認識,不必定正確,歡迎你評論,說出你的想法。
那麼,函數是對象嗎?是!由於它符合上面的定義:
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]]
,則它是一個普通對象;
函數肩負了兩項職責:
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背後必定有着「隱蔽」的內在聯繫。
計算機執行任何邏輯都須要成本:時間成本和內存成本。一樣一件事兩種作法,顯然咱們會選擇成本更低的那種作法。一樣的,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
的過程當中,是怎樣操做這個原型的。
引自MDN中的定義,new運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。
管它是啥,咱們關心的是new
內部的貓膩。 以let dog = new Dog('旺財')
爲例,new
進行了以下操做:
obj = {}
;obj
的內部屬性[[Prototype]]
指向構造函數Dog
的原型,即obj.[[Prototype]] = Dog.prototype
;Dog
的this
指向這個空對象obj
;Dog
,函數內部對this
的操做等同於操做obj
對象;Dog
指定了返回值,則正常返回這個返回值;不然,返回obj
對象;以上就是new
操做符的黑魔法,它已經幫咱們完成了對象實例化後原型的綁定,完成了「代理」指定。 其中,提到了obj
空對象的內部屬性[[Prototype]]
,它在new
過程當中會被指向構造函數的原型,至關於這個空對象obj
繼承了原型對象。 依據ECMA標準中的描述,每一個對象都會有這個[[Prototype]]
內部屬性,爲的就是實現對象繼承。
既然是內部屬性,咱們天然沒法直接經過obj.[[Prototype]]
訪問到。目前有兩個方式能夠從一個對象obj
上獲取到它繼承的原型對象:
obj.__proto__
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]]
產生了「剪不斷理還亂」的關係。
每一個對象都有構造函數!
上面提到咱們能夠經過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 複製代碼
爲何
dog2
是String
的實例,留給你去思考啦
若是不顯式的調用constructor
好像也沒啥問題啊,那你能保證你使用的某個第三方庫就不會顯式的調用你傳遞給它的對象的constructor
嗎?
養成習慣,及時修正你的constructor
。
從上節中,大體清楚瞭如何經過new
構造函數來實例化一個對象,那麼做爲普通函數和普通對象的構造函數Function
和Object
是怎樣的關係?
首先,Function
和Object
是內置構造函數,因此它們是函數,毋庸置疑:
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的說明)
同時,咱們在標準中也獲取到以下兩條信息:
- 每一個內置函數或內置構造函數的
[[Prototype]]
都指向Function.prototype
;- 每一個內置原型對象的
[[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的關係圖以下:
討論了這麼多,思緒有點亂了,我將全部關係者整合到一張關係圖中:
從這張關係圖中,咱們能夠概括出幾個信息點:
prototype
屬性,原型是一個對象,包含全部實例對象共享的屬性或方法;[[Prototype]]
內部屬性,指向原型對象,其中Object.prototype.[[Prototype]]
指向null
,以表示原型查找的終點;prototype
對象上掛載了constructor
屬性,指向構造函數;那麼當咱們在談論原型鏈的時候,究竟咱們在談論什麼?
若是你想真實看到這條「鏈」,那麼它就是關係圖中的粉色虛線框所示,可是這裏原型鏈只是賓語,談論原型鏈時,不能夠丟失主語,即foo對象的原型鏈
。直觀的說,原型鏈就是對象間用[[Prototype]]
內部屬性串聯起來的單向鏈表。
因此,函數
Foo
的原型鏈是怎樣的,我想你也能在關係圖找到。 函數Foo
的原型鏈能夠訪問到Function.prototype.call
或Function.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」來完成多重繼承?
碼字不易,若是:
您的支持與關注,是我持續創做的最大動力!