前言
首先原型、原型鏈,算是前端進階裏面必不可少,十分重要的一塊了。由於這塊特別繞,所以面試官很喜歡用這一塊來辨別你的底層知識掌握的怎麼樣。用的第三方框架,庫裏面,很多功能模塊化了,但大部分功能都繼承自一個基類。既然涉及到繼承,那也必不可少得先了解原型鏈,所以原型鏈確實重中之重。
為什麼大家對原型,原型鏈子會感到“懵”跟“繞”
其本質是因為,大家都沒理清楚__proto__、prototype、constructor三者的聯繫。所以很多人在看這一塊知識的時候,剛開頭看可能還能理解,看久了就懵了,因為代碼中充斥着各種x.__proto__.__proto__,x.__proto__.constructor.prototype,x.prototype.__proto__等等,這當然會懵掉。所以我們要理解原型、原型鏈是什麼,一定要先搞明白,__proto__、prototype、constructor這三個到底是個什麼東西,再弄明白它們三個是什麼聯繫。
下面我會用比較通俗的話來解釋,帶着大家更好的理解原型,原型鏈是什麼(因為為了大家更好的理解,所以有些地方可能會稍微有點不恰當,敬請見諒)。
為了更好的理解,我們用以下變量作為例子跟話術:
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__.prototype、People.prototype、Object.prototype、Function.prototype等描述,但一定不會看到實例.prototype(person.prototype)。
例外:
- 箭頭函數沒有
prototype,箭頭函數也不能拿來做構造函數- 使用
bind方法創造出來的副本函數也沒有prototype
這兩個是例外,大家記得就好,但不影響我們的理解。
constructor
這個屬性也可以通俗的理解成,所有對象都擁有的一個屬性。可以用對象.constructor.name來查看當前構造函數的名字是什麼(person.constructor.name返回People,因為person由People構造實例而來)。所以我們也會看到person.constructor、People.prototype.constructor、People.constructor等描述。
ok,介紹完這三個屬性,我們再來看看這三者有什麼聯繫。
__proto__、prototype、constructor這三者到底是什麼聯繫
我們看看下面例子:
// 定義一個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.prototypeprototype跟constructor是什麼聯繫:
constructor就是某個普通構造函數的prototype自身的一個屬性(用hasOwnProperty可驗證),它指向的就是這個構造函數本身,即People.prototype.constructor = People__proto__跟constructor是什麼聯繫:
__proto__跟constructor的聯繫跟prototype與constructor的聯繫一樣。因為以.__proto__結尾的,它最後一定指向某個構造函數的原型對象(People.prototype),然後又由於constructor是某個構造函數的prototype自身的一個屬性,因此我們可以這麼看:person.__proto__.constructor = People.prototype.constructor
ok,看到這裏,大家可以先暫停一下,整理一下思路。理一理什麼是__proto__、prototype、constructor;然後再理一理__proto__、prototype、constructor這三者之間的聯繫。然後接下來進入最讓我們蒙圈的東西——原型鏈。
什麼是原型鏈
當我們用構造函數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什麼的,先不要慌。
思想步驟:
- 我們直接看最後一個屬性,看看是以什麼結尾
- 然後再一步步反推前面調用的都是什麼對象
- 最後再推出它具體返回值的是什麼
規律:
- 如果最後以
.__proto__結尾,它最後返回的一定是某個構造函數的prototype(Object.prototype.__proto__除外,它到頂了,是原型鏈的頂端,返回null)
- 如果是以
.prototype結尾,那麼它前面一定是個構造函數,因為只有函數才會有prototype屬性(因為一般以.prototype結尾返回的都是這個構造函數的prototype所有的方法與屬性,所以題目很少會以.prototype結尾) -
如果是以
.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__);
-
我們以第一道題為例,解析一下:
- 先看是以什麼結尾。以
.__proto__ - ok,心裏有個大概了,根據規律總結第一點,它肯定返回某個構造函數的
prototype - 再反推一下前面調用的都是什麼對象。前面是
People,People是什麼?是構造函數,函數都有一個頂級基類構造函數,那就是Function,所以People.__proto__返回的就是Function.prototype。
- 先看是以什麼結尾。以
-
我們以第二道題為例,解析一下:
- 先看是以什麼結尾。以
.constructor - 調用對象直接是普通構造函數,根據規律總結第三點的第二小點,直接得出
Function
- 先看是以什麼結尾。以
-
我們再以第六道題為例,解析一下:
- 先看是以什麼結尾。以
.constructor - 再反推一下前面是調用的都是什麼對象。先看
person.__proto__,返回的是People.prototype,那這題就變成了People.prototype.__proto__.constructor。再繼續看,People.prototype.__proto__返回的是什麼,Object.prototype,那這題實際就是Object.prototype.constructor。根據規律總結第三點的第三小點,那它返回的就是Object本身。
- 先看是以什麼結尾。以
大家一定要注意,Object.__proto__跟Function.__proto__,Object跟Function都是頂級構造函數,所以Object.__proto__、Function.__proto__返回的都是Function.prototype
牛刀小試
根據上面對__proto__、prototype、constructor的特點總結,還有方法總結,我們可以拿下面這道題來試試,如果大家都可以正確無誤的答出來,那大家對原型應該就瞭解的差不多了
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__、prototype、constructor是什麼,再明白它們之間的是什麼聯繫,循環漸進。等理解以後,多畫幾遍原型鏈圖加深理解。OK,最後祭出一張原型鏈圖:
紅色鏈表示的就是實例person原型鏈
寫着寫着,發現又寫了一大堆,希望能夠幫助到大家。如果覺得覺得寫得好的,有幫助到的,歡迎大家點贊,也歡迎大家評論交流。
既然明白了什麼是原型鏈,那還不趕緊趁熱打鐵,進階看看什麼是JS繼承吧!