前端面試必備 | 古怪的原型(雞生蛋仍是蛋生雞)(原型篇:中)

本文翻譯自 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 中包含如下數據類型:面試

  1. boolean
  2. number
  3. string
  4. undefined
  5. null
  6. symbol
  7. bigint(new)
  8. object

上面的數據類型中除了對象,其餘的都是原始數據類型,他們存儲對應類型的數據。數組

而對象 object 是引用類型,咱們能夠將其描述爲鍵-值對的集合(事實上不只如此)。瀏覽器

在 JavaScript 中,可使用構造函數 (constructor) 或者對象字面量({})建立對象。閉包

JavaScript 中的函數是能夠 「調用」 的特殊對象。咱們使用 Function 構造函數或者函數聲明來建立函數。這些構造函數既是對象又是函數,這個問題始終讓我困惑,就像雞生蛋仍是蛋生雞同樣困惑着每一個人。函數

在開始瞭解原型以前,我想澄清一下 JavaScript 中有兩個原型:佈局

  1. prototype:這是一個特殊的對象,它是全部你建立的函數都會有的一個屬性。更準確點講,你建立的任何函數都已經存在該屬性,可是這個屬性對於 JavaScript 引擎自帶的函數或者 bind 產生的新函數倒是不必定會有的。這個 prototype 屬性所指向的對象與你用該構造函數建立的對象的 [[Prototype]] 屬性所指向的對象是同一個;
  2. [[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.prototypeFunction.prototype 也有一些函數屬性。如 Object.assignObject.prototype.hasOwnPropertyFunction.prototype.call,這些都是引擎內部函數,他們沒有 prototype 屬性,它們是 Function 的實例,它們有指向 Function.prototype__proto__ 屬性。

Object.create.__proto__ === Function.prototype
// true

你也能夠探索其餘的構造函數,如 ArrayDate,或者看看它們的實例的 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 不同:

  1. Function.prototype 是一個 JavaScript 引擎內置的函數對象;
  2. Array.prototype 是一個引擎內置的數組對象,而且內置有針對這種對象的一些方法。

函數原型對象是爲了兼容 ES6 以前的版本的 JS,這也不會讓 Function.prototype 成爲一個特別的函數。只有構造函數纔會有 prototype 屬性。

能做爲構造函數的函數必須有一個 prototype 屬性。

下面有一些非構造函數的例子。

  1. Math 對象的方法
typeof Math.pow; // "function
'prototype' in Math.pow; // false
  1. 一些宿主對象(host objects)
typeof document.createElement('object'); // "function
'prototype' in document.createElement('object'); // false
  1. ES6 中的箭頭函數(正是由於沒有 prototype 屬性,因此箭頭函數不能做爲構造函數使用
typeof (x => x * x); // "function
'prototype' in (x => x * x); // false

第二個:如何理解包裝對象
也能夠參考這篇文章 https://blog.csdn.net/lhjuejiang/article/details/79623505

在文章的最開始咱們列出了 JS 中的數據類型,其中(這裏不考慮 symbolbigintbooleannumberstringnullundefined 都是非引用類型,也就是說變量直接指向的是原始值。

咱們日常也會看到下面的操做:

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,帶你解讀前端技術,掌握最本質的技能。關注公衆號能夠拉你進討論羣,有任何問題都會回覆。

公衆號

相關文章
相關標籤/搜索