原型鏈的概念
由于在JS世界中,函數(shù)其實(shí)也是個(gè)對(duì)象,所以函數(shù)可以擁有屬性,JS規(guī)定了所有的函數(shù)都默認(rèn)擁有一個(gè)叫做prototype的屬性,這個(gè)屬性指向了另一個(gè)對(duì)象。比如我們聲明一個(gè)function:
function Person() {}
console.log(Person.prototype);
打印出來(lái)的結(jié)果為:

可以看到,函數(shù)的prototype默認(rèn)擁有兩個(gè)屬性:constructor和__proto__;我們可以給函數(shù)的prototype對(duì)象添加更多的自定義屬性,比如,我想給Person函數(shù)添加一個(gè)getName方法,可以這么做:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
console.log(Person.prototype);
現(xiàn)在prototype里面已經(jīng)有個(gè)自定義的方法了:

我們可以通過(guò)
new關(guān)鍵字實(shí)例化一個(gè)對(duì)象,比如上面代碼可以這樣寫:const p = new Person('張三'),此時(shí),Person函數(shù)被稱為構(gòu)造函數(shù),可以理解為是用來(lái)構(gòu)造對(duì)象的函數(shù),p被稱為實(shí)例對(duì)象,現(xiàn)在函數(shù)內(nèi)部的this指向的就是這個(gè)實(shí)例對(duì)象了,p.name得到的結(jié)果便是“張三”。
可以通過(guò)instanceof判斷一個(gè)對(duì)象是否是某個(gè)構(gòu)造函數(shù)的實(shí)例:
p instanceof Person; // true
注意,構(gòu)造函數(shù)首字母需要大寫,所以截圖中的構(gòu)造函數(shù)聲明是不規(guī)范的。
令人困惑的是,我們可以通過(guò)這個(gè)實(shí)例對(duì)象去調(diào)用綁定在Person.prototype上的方法,比如上述例子中通過(guò)p.getName()也可以拿到結(jié)果“張三”,看起來(lái)似乎是創(chuàng)建p的時(shí)候會(huì)把Person.prototype對(duì)象復(fù)制到這個(gè)實(shí)例對(duì)象中。然而事實(shí)并不是這樣。
我們可以console.log(p)查看下p中的內(nèi)容,得到結(jié)果:

可以看到:p實(shí)例對(duì)象中有一個(gè)默認(rèn)的__proto__對(duì)象,而getName方法就在__proto__對(duì)象中。在JavaScript中,每個(gè)實(shí)例對(duì)象都有一個(gè)私有屬性__proto__,指向它的構(gòu)造函數(shù)的原型對(duì)象prototype。
p.__proto__ === Person.prototype; // true
事實(shí)上,所有的對(duì)象都具有__proto__屬性,因?yàn)樗械膶?duì)象都是Object的實(shí)例對(duì)象:
var o = {};
o instanceof Object; // true
o.__proto__ === Object.prototype; // true
重點(diǎn): 當(dāng)試圖訪問(wèn)一個(gè)對(duì)象的屬性時(shí),它會(huì)先在該對(duì)象本身內(nèi)查找,如果沒(méi)有查到,則會(huì)在該對(duì)象的原型上查找,如果還沒(méi)找到,則繼續(xù)在該對(duì)象的原型的原型上查找,層層向上直到一個(gè)原型對(duì)象為null。這條“鏈”我們稱之為原型鏈。
好了,根據(jù)上面的描述,我們來(lái)看下這段代碼的原型查找是怎么樣的:
function Person(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
const p = new Person('張三');
p.toString(); // [object Object]
- p查找本身有沒(méi)有toString方法
- p本身沒(méi)有該方法,則查找其構(gòu)造函數(shù)原型上有無(wú)此方法
-
p.__proto__上也沒(méi)有,則繼續(xù)查找p.__proto__.__proto__對(duì)象上有無(wú)此方法,找到并調(diào)用:
image.png
由上所知,下面這個(gè)等式也是成立的:
Person.prototype.__proto__ === Object.prototype; // true
ok,還有個(gè)問(wèn)題,Person也是個(gè)對(duì)象,那Person.__proto__指向的是誰(shuí)呢?答案是Function.prototype,所有的函數(shù)都是Function的實(shí)例,而Function.__proto__指向Function.prototype:
Person instanceof Function; // true
Person.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
上述便是一個(gè)構(gòu)造函數(shù)的整個(gè)原型鏈的結(jié)構(gòu),簡(jiǎn)單總結(jié)一下:
- 所有的JS對(duì)象都存在
__proto__屬性,只有函數(shù)對(duì)象才有prototype屬性,所以函數(shù)對(duì)象既有__proto__也有prototype - 構(gòu)造函數(shù)的實(shí)例對(duì)象的
__proto__指向構(gòu)造函數(shù)的prototype,普通對(duì)象都是Object的實(shí)例,在沒(méi)有特別處理的情況下,普通對(duì)象的__proto__指向Object.prototype
繼承
這里我們只討論基于prototype的繼承,假設(shè)我們有兩個(gè)構(gòu)造函數(shù):
// 定義一個(gè)父類
function Parent() {}
Parent.prototype.getName = function() {
return 'parent\'s name';
}
// 定義一個(gè)子類
function Child() {}
我們可以直接通過(guò)prototype賦值的寫法進(jìn)行繼承:
Child.prototype = Parent.prototype;
這樣,所有掛載在Parent.prototype上的方法和屬性都能被繼承下來(lái)了:
const child = new Child();
console.log(child.getName()); // parent's name
但這種做法有兩個(gè)缺點(diǎn),明顯的是,直接用prototype賦值之后,Child.prototype和Parent.prototype現(xiàn)在指向了同一個(gè)對(duì)象,任何對(duì)Child.prototype的更改都會(huì)直接影響Parent.prototype;還有個(gè)不明顯的缺點(diǎn),上文中說(shuō)到,函數(shù)的prototype有個(gè)默認(rèn)的屬性叫做constructor,指向的是構(gòu)造器本身:
Child.prototype.constructor === Child; // true
Parent.prototype.constructor === Parent; // true
賦值之后,Child.prototype.constructor的指向變成了Parent!一般的做法是手動(dòng)將指向改回來(lái):
Child.prototype.constructor = Child;
但我們并沒(méi)有解決兩個(gè)prototype綁定在了一起的問(wèn)題。我們可以用另一種基于prototype的方法實(shí)現(xiàn)繼承,即使用Object.create()方法:
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
});
關(guān)于Object.create()的詳細(xì)解釋,請(qǐng)看MDN。上述的意思大致是創(chuàng)造一個(gè)基于Parent.prototype的對(duì)象并賦值給Child.prototype,其中第二個(gè)參數(shù)的意思是設(shè)置這個(gè)新創(chuàng)造對(duì)象的constructor屬性,里面的設(shè)置內(nèi)容對(duì)應(yīng)Object.defineProperty的第三個(gè)參數(shù)(即屬性描述符),可以看到這里設(shè)置了constructor屬性的value為Child,即將Child.prototype.constructor指向Child;
上述代碼還需要完善下,因?yàn)槲覀冸m然實(shí)現(xiàn)了Child繼承了Parent,且解決了prototype的指向問(wèn)題,但是Child.__proto__現(xiàn)在仍然指向的是Function.prototype,為了繼承的完整性,我們需要將Child.__proto__指向Parent:
Object.setPrototypeOf
? Object.setPrototypeOf(Child, Parent)
: Child.__proto__ = Parent;
這個(gè)基于Object.create()的繼承方式,就是ES6的class中extends的實(shí)現(xiàn)原理。
在普通對(duì)象中使用Object.create(),比如:
const origin = {
name: 'origin'
};
const o = Object.create(origin);
此時(shí)o.__proto__指向的不再是Object.prototype了,而是origin,并且o已經(jīng)繼承了origin對(duì)象的所有屬性(無(wú)論是否可枚舉):
o.__proto__ === origin; // true
console.log(o.name); // 'origin'
我們可以使用Object.getPrototypeOf()獲取當(dāng)前對(duì)象的原型:
console.log(Object.getPrototypeOf(o)); // {name: "origin'}
可以使用Object.getOwnPropertyNames()獲取一個(gè)對(duì)象的自身屬性(即不是繼承下來(lái)的屬性),返回的是自身所有屬性(無(wú)論是否可枚舉)的數(shù)組:
console.log(Object.getOwnPropertyNames(o)); // []
console.log(Object.getOwnPropertyNames(origin)); // ["name"]
可以看到, Object.getOwnPropertyNames()與Object.keys()相比,后者僅返回自身的可枚舉屬性,而不可枚舉不會(huì)被返回。
到這里,差不多已經(jīng)把JS中原型的概念都介紹了,看完這篇文章并且能真的理解的話,JS原型的知識(shí)肯定不成問(wèn)題了。
