JS深入學(xué)習(xí) — prototype

原型鏈的概念

由于在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é)果為:


image.png

可以看到,函數(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è)自定義的方法了:

image.png

我們可以通過(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é)果:

image.png

可以看到: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]
  1. p查找本身有沒(méi)有toString方法
  2. p本身沒(méi)有該方法,則查找其構(gòu)造函數(shù)原型上有無(wú)此方法
  3. 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.prototypeParent.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的classextends的實(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)題了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容