動態

詳情 返回 返回

關於原型鏈的問題,教你怎麼套用方法直接判斷,面試不再虛 - 動態 詳情

前言

首先原型、原型鏈,算是前端進階裏面必不可少,十分重要的一塊了。由於這塊特別繞,所以面試官很喜歡用這一塊來辨別你的底層知識掌握的怎麼樣。用的第三方框架,庫裏面,很多功能模塊化了,但大部分功能都繼承自一個基類。既然涉及到繼承,那也必不可少得先了解原型鏈,所以原型鏈確實重中之重。

為什麼大家對原型,原型鏈子會感到“懵”跟“繞”

其本質是因為,大家都沒理清楚__proto__prototypeconstructor三者的聯繫。所以很多人在看這一塊知識的時候,剛開頭看可能還能理解,看久了就懵了,因為代碼中充斥着各種x.__proto__.__proto__x.__proto__.constructor.prototypex.prototype.__proto__等等,這當然會懵掉。所以我們要理解原型、原型鏈是什麼,一定要先搞明白,__proto__prototypeconstructor這三個到底是個什麼東西,再弄明白它們三個是什麼聯繫。

下面我會用比較通俗的話來解釋,帶着大家更好的理解原型,原型鏈是什麼(因為為了大家更好的理解,所以有些地方可能會稍微有點不恰當,敬請見諒)。

為了更好的理解,我們用以下變量作為例子跟話術:

  • People為構造函數
  • person為由People實例出來的一個對象
  • Object為構造所有對象的頂級基類構造函數
  • Function為構造所有函數的頂級基類構造函數

__proto__

這個屬性可以通俗的理解成,所有對象都擁有的一個私有屬性(函數也是一種特殊的對象,所以構造函數也會有這個屬性)。所以我們會看到person.__proto__People.prototype.__proto__People.__proto__Object.__proto__Function.__proto__等描述。

prototype

這個屬性可以通俗的理解成,專屬於函數自身的一個屬性(可用hasOwnProperty驗證),所以實例出來的對象不會有,只有函數、構造函數會有。我們通常都會把構造函數.prototype看做一個整體,它代表的是,這個函數的prototype裏,所有的屬性方法等(People.prototype代表People.prototype這個整體裏,所有的屬性與方法)。所以我們會看到person.__proto__.prototypePeople.prototypeObject.prototypeFunction.prototype等描述,但一定不會看到實例.prototypeperson.prototype)。

例外:

  • 箭頭函數沒有prototype,箭頭函數也不能拿來做構造函數
  • 使用bind方法創造出來的副本函數也沒有prototype

這兩個是例外,大家記得就好,但不影響我們的理解。

constructor

這個屬性也可以通俗的理解成,所有對象都擁有的一個屬性。可以用對象.constructor.name來查看當前構造函數的名字是什麼(person.constructor.name返回People,因為personPeople構造實例而來)。所以我們也會看到person.constructorPeople.prototype.constructorPeople.constructor等描述。

ok,介紹完這三個屬性,我們再來看看這三者有什麼聯繫。

__proto__prototypeconstructor這三者到底是什麼聯繫

我們看看下面例子:

// 定義一個People構造函數
function People () {

}

// 實例化一個person對象
const person = new People();

// 打印true --> 説明實例的__proto__與實例的構造函數的prototype相等
console.log(person.__proto__ === People.prototype); 

// 打印true --> 説明constructor是構造函數的prototype裏“自身”的一個屬性
console.log(People.prototype.hasOwnProperty('constructor'));

// 打印true --> 説明非頂級構造函數的prototype.constructor指回這個構造函數本身
console.log(People.prototype.constructor === People);

// 打印true --> 説明實例的__proto__.constructor 就是 構造函數的prototype.constructor(由第一個打印可知person.__proto__ = People.prototype)
console.log(person.__proto__.constructor === People.prototype.constructor);

// 打印People --> 説明實例的constructor指向的就是實例的構造函數
console.log(person.constructor.name);

// 打印fale --> 説明實例自身是沒有的constructor屬性的
console.log(person.hasOwnProperty('constructor'));

// 打印true, true --> 説明實例自身是沒有的constructor屬性的
// 它是繼承自實例的__proto__.constructor,即實例的構造函數的prototype.constructor
console.log(person.constructor === person.__proto__.constructor, person.constructor === People.prototype.constructor);

解析:

  • __proto__prototype是什麼聯繫:
    如果有一個實例,它是由一個構造函數實例而來,那麼這個實例的__proto__一定指向這個構造函數的prototype,即person.__proto__ = People.prototype
  • prototypeconstructor是什麼聯繫:
    constructor就是某個普通構造函數的prototype自身的一個屬性(用hasOwnProperty可驗證),它指向的就是這個構造函數本身,即People.prototype.constructor = People
  • __proto__constructor是什麼聯繫:
    __proto__constructor的聯繫跟prototypeconstructor的聯繫一樣。因為以.__proto__結尾的,它最後一定指向某個構造函數的原型對象(People.prototype,然後又由於constructor是某個構造函數的prototype自身的一個屬性,因此我們可以這麼看:person.__proto__.constructor = People.prototype.constructor

ok,看到這裏,大家可以先暫停一下,整理一下思路。理一理什麼是__proto__prototypeconstructor;然後再理一理__proto__prototypeconstructor這三者之間的聯繫。然後接下來進入最讓我們蒙圈的東西——原型鏈。

什麼是原型鏈

當我們用構造函數People實例化了一個對象person後,訪問person的方法或者屬性時,會先在實例person自身找有沒有對應的方法屬性。有值的話,則返回值,沒有的話則去person.__proto__People.prototype)裏找;有值的話,則返回值,沒有的話,又會去People.prototype.__proto__Object.prototype)裏找。有值的話,則返回值;沒有的話,又會去Object.prototype._proto__裏找,但是Object.prototype.__proto__返回null,原型鏈到頂,一條條原型鏈搜索完畢,都沒有,則返回undefined

在查找的過程中會遍歷以上的一條鏈,這條鏈就是原型鏈。上述的過程可以這麼看(這個過程也是實現繼承的核心):

經過上述的知識點,相信大家對原型鏈應該有個基本的認識裏吧,現在我們來總結一下,看看有沒有什麼方法規律。

方法總結

在看到一堆類似.__proto__.__proto__.__proto__.__proto__.__proto__.prototype.__proto__.prototype.consturtor什麼的,先不要慌。

思想步驟:
  1. 我們直接看最後一個屬性,看看是以什麼結尾
  2. 然後再一步步反推前面調用的都是什麼對象
  3. 最後再推出它具體返回值的是什麼
規律:
  1. 如果最後以.__proto__結尾,它最後返回的一定是某個構造函數的prototypeObject.prototype.__proto__除外,它到頂了,是原型鏈的頂端,返回null
  1. 如果是以.prototype結尾,那麼它前面一定是個構造函數,因為只有函數才會有prototype屬性(因為一般以.prototype結尾返回的都是這個構造函數的prototype所有的方法與屬性,所以題目很少會以.prototype結尾)
  2. 如果是以.constructor結尾,先弄清楚前面是什麼

    • 如果前面是實例,那它直接返回創造實例的那個構造函數;
    • 如果前面直接是頂級基類構造函數Function.constructor)或者直接是普通構造函數People.constructor),它會直接指向構造所有函數的頂級基類構造函數Function(所有構造函數都是函數,都由頂級構造函數Function而來,所以constructor當然指向它;
    • 如果前面是非頂級構造函數(普通函數)的原型對象People.prototype.constructor),因為實例的constructor是繼承自普通構造函數.prototype.constructor,所以普通構造函數.prototype.constructor必須指回它自己,(普通構造函數.prototype.constructor = 普通構造函數)。針對這點,我們看看它是怎麼繼承來的。

    constructor整個繼承的流程是:在實例person本身查找,找不到去person.__proto__People.prototype)找,發現有People.prototype.constructor,並且People.prototype.constructor = People返回它,所以person.constructor = People
    流程如圖所示:

ok,經過上面總結出的思想步驟跟規律,我們來試試:

// 定義一個People構造函數
function People() {

}

// 實例化一個person對象
const person = new People();

// 第一題
console.log(People.__proto__);

// 第二題
console.log(People.constructor);

// 第三題
console.log(person.prototype);

// 第四題
console.log(person.__proto__.__proto__);

// 第五題
console.log(People.__proto__.prototype);

// 第六題
console.log(person.__proto__.__proto__.constructor);

// 第七題
console.log(Object.__proto__);
  • 我們以第一道題為例,解析一下:

    1. 先看是以什麼結尾。以.__proto__
    2. ok,心裏有個大概了,根據規律總結第一點,它肯定返回某個構造函數的prototype
    3. 再反推一下前面調用的都是什麼對象。前面是PeoplePeople是什麼?是構造函數,函數都有一個頂級基類構造函數,那就是Function,所以People.__proto__返回的就是Function.prototype
  • 我們以第二道題為例,解析一下:

    1. 先看是以什麼結尾。以.constructor
    2. 調用對象直接是普通構造函數,根據規律總結第三點的第二小點,直接得出Function
  • 我們再以第六道題為例,解析一下:

    1. 先看是以什麼結尾。以.constructor
    2. 再反推一下前面是調用的都是什麼對象。先看person.__proto__,返回的是People.prototype,那這題就變成了People.prototype.__proto__.constructor。再繼續看,People.prototype.__proto__返回的是什麼,Object.prototype,那這題實際就是Object.prototype.constructor。根據規律總結第三點的第三小點,那它返回的就是Object本身。
大家一定要注意,Object.__proto__Function.__proto__ObjectFunction都是頂級構造函數,所以Object.__proto__Function.__proto__返回的都是Function.prototype

牛刀小試

根據上面對__proto__prototypeconstructor的特點總結,還有方法總結,我們可以拿下面這道題來試試,如果大家都可以正確無誤的答出來,那大家對原型應該就瞭解的差不多了

function Person(name) {
    this.name = name
}
var p2 = new Person('king');

console.log(p2.__proto__); // Person.prototype

console.log(p2.__proto__.__proto__); // Object.prototype

console.log(p2.__proto__.__proto__.__proto__); // null

console.log(p2.__proto__.__proto__.__proto__.__proto__); // 報錯

console.log(p2.constructor); // Person

console.log(p2.prototype); // undefined

console.log(Person.constructor); // Function

console.log(Person.prototype); // 輸出Person.prototype這個對象裏所有的方法和屬性

console.log(Person.prototype.constructor); // Person

console.log(Person.prototype.__proto__); // Obejct.prototype

console.log(Person.__proto__); // Fuction.prototype

console.log(Function.prototype.__proto__); // Obeject.prototype

console.log(Function.__proto__); // Function.prototype

console.log(Object.__proto__); // Function.prototype

console.log(Object.prototype.__proto__); // null

最後

原型、原型鏈本來就挺繞的,所以大家先了解__proto__prototypeconstructor是什麼,再明白它們之間的是什麼聯繫,循環漸進。等理解以後,多畫幾遍原型鏈圖加深理解。OK,最後祭出一張原型鏈圖:

紅色鏈表示的就是實例person原型鏈

寫着寫着,發現又寫了一大堆,希望能夠幫助到大家。如果覺得覺得寫得好的,有幫助到的,歡迎大家點贊,也歡迎大家評論交流。

既然明白了什麼是原型鏈,那還不趕緊趁熱打鐵,進階看看什麼是JS繼承吧!

user avatar cyzf 頭像 alibabawenyujishu 頭像 zaotalk 頭像 nihaojob 頭像 cicadasmile 頭像 razyliang 頭像 longlong688 頭像 huajianketang 頭像 banana_god 頭像 anchen_5c17815319fb5 頭像 Dream-new 頭像 xiaoxxuejishu 頭像
點贊 74 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.