本文翻譯自 https://medium.com/free-code-camp/prototype-in-js-busted-5547ec68872,做者 Pranav Jindal
,翻譯時有刪改,標題有改動。
如下四行足以使大多數 JavaScript 開發人員感到困惑:javascript
Object instanceof Function // true Object instanceof Object // true Function instanceof Object // true Function instanceof Function // true
JavaScript 中的原型是極其難以理解的概念之一,可是你不能逃避它。無論你怎麼忽略,你終究會在開發過程當中碰到原型難題。前端
因此,讓咱們直面它吧。java
從基礎開始,JavaScript 中包含如下數據類型:面試
上面的數據類型中除了對象,其餘的都是原始數據類型,他們存儲對應類型的數據。數組
而對象 object
是引用類型,咱們能夠將其描述爲鍵-值對的集合(事實上不只如此)。瀏覽器
在 JavaScript 中,可使用構造函數 (constructor
) 或者對象字面量({}
)建立對象。閉包
JavaScript 中的函數是能夠 「調用」 的特殊對象。咱們使用 Function
構造函數或者函數聲明來建立函數。這些構造函數既是對象又是函數,這個問題始終讓我困惑,就像雞生蛋仍是蛋生雞同樣困惑着每一個人。函數
在開始瞭解原型以前,我想澄清一下 JavaScript 中有兩個原型:佈局
prototype
:這是一個特殊的對象,它是全部你建立的函數都會有的一個屬性。更準確點講,你建立的任何函數都已經存在該屬性,可是這個屬性對於 JavaScript 引擎自帶的函數或者 bind
產生的新函數倒是不必定會有的。這個 prototype
屬性所指向的對象與你用該構造函數建立的對象的 [[Prototype]]
屬性所指向的對象是同一個;[[Prototype]]
:這是每一個對象都有的隱藏屬性,若是在對象上沒法讀取到某個屬性,則 JavaScript 引擎會嘗試從對象的 [[Prototype]]
屬性指向的對象上繼續查找。實例的 這個屬性所指向的對象和構造函數的 prototype
屬性指向的對象是同一個。[[Prototype]]
是給引擎內部使用的,在咱們編寫的 JS 腳本中可使用 __proto__
屬性訪問原型對象。還有其餘訪問此原型的新方法,可是爲了簡潔起見,我將用 __proto__
代替 [[Prototype]]
來作講解;var obj = {} // 對象字面量 var obj1 = new Object() // 構造函數建立對象
上面兩個語句對於建立一個新的對象來說是同樣的,事實上當咱們執行上面任何一條語句的時候都發生了不少事情。this
當我建立一個新對象的時,建立的是一個空對象。事實上,它並非空的,由於它是對象構造函數 Object
的一個實例,所以它自己會有一個屬性指向 Object.prototype
,而這個屬性就是 __proto__
。
若是咱們查看 Object
構造函數的 prototype
屬性,你會發現它和 obj.__proto__
如出一轍。事實上他們是兩個不一樣的指針指向了相同的對象。
obj.__proto__ === Object.prototype // true
每一個函數的 prototype
屬性都會有一個 constructor
屬性,這個屬性都是指向的函數本身。對於 Object
函數,prototype
有一個 constructor
屬性指回了 Object
函數。
Object.prototype.constructor === Object //true
在上面的圖片中,左邊是 Object
構造函數展開後的。你可能會感到疑惑,裏面怎麼有這麼多函數。函數其實也是對象,所以它也能夠像對象同樣擁有各類屬性。
若是你仔細看,你會發現 Object
(左邊的)有一個 __proto__
屬性,這意味着 Object
確定也是由其餘有 prototype
的構造函數建立的。因爲 Object
是一個函數對象,因此它確定是由 Function
構造函數建立的。
Object.__proto__
看起來和 Function.prototype
同樣。當我檢查二者是否全等時,發現它們確實是指向的同一個對象。
Object.__proto__ === Function.prototype //true
若是你仔細的看上面的圖,你也會發現 Function
自己也有一個 __proto__
屬性,這意味着 Function
構造函數也必定由其餘有 prototype
的構造函數建立而來。因爲 Function
自己是一個函數,它確定是經過 Function
構造函數建立而來,也就是說,它本身建立了本身。這看起來比較荒謬,可是當你檢查的時候,它確實是本身建立了本身。
Function
的 __proto__
和 prototype
實際上指向了相同的對象,也就是 Function
的原型對象。
Function.prototype === Function.__proto__ // true
文章前面也說過,函數的 prototype.constructor
屬性必然會指向這個函數。
Function.prototype.constructor === Function // true
上面這張圖很是有趣!!
咱們再來捋一遍,Function.prototype
也有一個 __proto__
屬性。好吧,這也沒什麼讓人驚訝的,畢竟 prototype
是一個對象,它確定能夠有一個這個屬性。可是注意,這個屬性也是指向 Object.prototype
的。
Function.prototype.__proto__ == Object.prototype // true
因此有了下面這張圖:
// instanceof 操做符 a instanceof b
instanceof
操做符會查找 a
的原型鏈上的任何 constructor
屬性。只要找到了 b
就會返回 true
,不然返回 false
。
如今咱們回到文章最開始的四個 instanceof
語句。
Object instanceof Function Object.__proto__.constructor === Function Object instanceof Object Object.__proto__.__proto__.constructor === Object Function instanceof Function Function.__proto__.constructor === Function Function instanceof Object Function.__proto__.__proto__.constructor === Object
上面的狀況太讓人糾結了,哈哈!!可是我但願能簡單點理解。
這裏我有一點沒有提出來,那就是 Object.prototype
沒有 __proto__
屬性。
事實上,它其實有一個 __proto__
屬性指向 null
。原型鏈查找最終會在找到 null
以後中止查找。
Object.prototype.__proto__ // null
Object
, Function
, Object.prototype
和 Function.prototype
也有一些函數屬性。如 Object.assign
、Object.prototype.hasOwnProperty
、Function.prototype.call
,這些都是引擎內部函數,他們沒有 prototype
屬性,它們是 Function
的實例,它們有指向 Function.prototype
的 __proto__
屬性。
Object.create.__proto__ === Function.prototype // true
你也能夠探索其餘的構造函數,如 Array
、Date
,或者看看它們的實例的 prototype
和 __proto__
。我肯定你能夠發現這些功能內在的聯繫。
額外的問題:
這裏有幾個困擾我一段時間的問題:爲何 Object.prototype
是普通對象而 Function.prototype
是函數對象。
這裏 https://stackoverflow.com/a/32929083/1934798 給出瞭解答。
另外一個問題是:原始數據類型是如何調用對應的方法的,如 toString()
、substr()
、toFixed()
?這裏 https://javascript.info/native-prototypes#primitives 給出瞭解釋。(譯者注:也叫如何理解包裝對象)
我把上面兩個問題貼到這裏。
第一個: 爲何 Function.prototype
是一個函數對象而 Object.prototype
是一個普通對象?
在 ES6 中 Array.prototype
Function.prototype
和其餘的構造函數的 prototype
不同:
Function.prototype
是一個 JavaScript 引擎內置的函數對象;Array.prototype
是一個引擎內置的數組對象,而且內置有針對這種對象的一些方法。函數原型對象是爲了兼容 ES6 以前的版本的 JS,這也不會讓 Function.prototype
成爲一個特別的函數。只有構造函數纔會有 prototype
屬性。
能做爲構造函數的函數必須有一個 prototype
屬性。
下面有一些非構造函數的例子。
Math
對象的方法typeof Math.pow; // "function 'prototype' in Math.pow; // false
typeof document.createElement('object'); // "function 'prototype' in document.createElement('object'); // false
prototype
屬性,因此箭頭函數不能做爲構造函數使用)typeof (x => x * x); // "function 'prototype' in (x => x * x); // false
第二個:如何理解包裝對象
也能夠參考這篇文章 https://blog.csdn.net/lhjuejiang/article/details/79623505。
在文章的最開始咱們列出了 JS 中的數據類型,其中(這裏不考慮 symbol
和 bigint
) boolean
、number
、string
、null
、undefined
都是非引用類型,也就是說變量直接指向的是原始值。
咱們日常也會看到下面的操做:
var str = 'hello'; //string 基本類型 var s2 = str.charAt(0); alert(s2); // h
上面的 string
是一個基本類型,可是它卻能召喚出一個 charAt()
的方法,這是什麼緣由呢?
主要是由於:字符串去調方法的時候,基本類型會找到對應的包裝對象類型,而後包裝對象把全部的屬性和方法給了基本類型,而後包裝類型消失。
其過程大概是下面這樣:
var str = 'hello'; //string 基本類型 var s2 = str.charAt(0); //在執行到這一句的時候 後臺會自動完成如下動做 : ( var str = new String('hello'); // 1 找到對應的包裝對象類型,而後經過包裝對象建立出一個和基本類型值相同的對象 var s2 = str.chaAt(0); // 2 而後這個對象就能夠調用包裝對象下的方法,而且返回結給s2. str = null; // 3 以後這個臨時建立的對象就被銷燬了, str =null; ) alert(s2);// h alert(str);// hello 注意這是一瞬間的動做 實際上咱們沒有改變字符串自己的值。
也就是說,當原始值須要用到包裝對象的屬性或者方法的時候,會構造一個臨時的包裝對象出來,使用了以後就銷燬了。因此即便給這個原始值賦值,因爲賦值以後對象會被銷燬,以後從這個原始值上並不能獲取到對應的屬性。
看下面的面試題:把原始值當作一個對象用的時候,所使用的的方法會對隱式產生的包裝對象起做用,但不對原始值起做用。
var str="hello"; str.number = 10; // 包裝對象消失 alert(str.number); // undefined
往期精彩:
關注公衆號能夠看更多哦。
感謝閱讀,歡迎關注個人公衆號 雲影 sky,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。