JavaScript 的原型鏈機(jī)制與繼承詳解
JavaScript 的原型和原型鏈,本質(zhì)是一種基于對(duì)象的繼承機(jī)制。通過原型鏈實(shí)現(xiàn)屬性查找和方法復(fù)用,使多個(gè)對(duì)象能夠共享同一份數(shù)據(jù)和行為,同時(shí)避免重復(fù)創(chuàng)建,提高性能。
原型
官方定義(MDN):
當(dāng)使用 new 調(diào)用函數(shù)時(shí),構(gòu)造函數(shù)的 prototype 屬性將成為結(jié)果對(duì)象的原型。
原型是幾乎每個(gè)對(duì)象實(shí)例都有的內(nèi)部屬性 [[Prototype]](可以通過定義在 Object.prototype 上的訪問器屬性 .__proto__訪問),在通過 new 創(chuàng)建對(duì)象時(shí)這個(gè)屬性就指向構(gòu)造函數(shù)的 prototype 屬性。
訪問器屬性: 這是與普通屬性(數(shù)據(jù)屬性)不同的概念,數(shù)據(jù)屬性用于存儲(chǔ)值,而訪問器屬性通過 getter/setter 定義屬性的讀取和賦值行為。普通屬性例如
obj.name = "G.E.M."就像是一個(gè)箱子將對(duì)應(yīng)的值存儲(chǔ)起來,而訪問器屬性并不是一個(gè)儲(chǔ)物箱而是訪問器。
從底層來看,.__proto__ 其實(shí)是通過 getter/setter 這兩個(gè)函數(shù)觸發(fā)了引擎內(nèi)部方法 [[GetPrototypeOf]] 和 [[SetPrototypeOf]]從而間接讀寫內(nèi)部屬性 [[Prototype]]。ES5 標(biāo)準(zhǔn)更推薦 Object.getPrototypeOf() 和 Object.setPrototypeOf() 這兩個(gè)方法讀寫對(duì)象實(shí)例的原型,.__proto__ 僅為保證瀏覽器向下兼容而存在。
這里值得注意的是,構(gòu)造函數(shù)也有原型,即內(nèi)部屬性 [[Prototype]],但這和它的 prototype 屬性并不是一個(gè)東西。構(gòu)造函數(shù)本質(zhì)上是對(duì)象,所以它自然也有原型,而 prototype 屬性是普通函數(shù)(非箭頭函數(shù))在創(chuàng)建時(shí)自帶的、用來為未來實(shí)例當(dāng)作公共池子的普通屬性,用于實(shí)現(xiàn)對(duì)象的繼承。
而原型對(duì)象上有個(gè) constructor 屬性指向構(gòu)造函數(shù)本身。也就是說,在默認(rèn)情況下,如果假設(shè) const obj = new Foo(),那么就存在 Object.getPrototypeof(obj).constructor === Foo。
綜上所述,每個(gè)對(duì)象身上都有一個(gè)內(nèi)部屬性 [[Prototype]],指向一個(gè)對(duì)象,即原型對(duì)象。想要訪問到這個(gè)對(duì)象可以通過 Object.getPrototypeof() 或者 .__proto__,這兩個(gè)方法都會(huì)指向原型對(duì)象。并且,在實(shí)例被創(chuàng)建時(shí),會(huì)將其原型指向構(gòu)造函數(shù)的普通屬性 prototype,而在這個(gè)原型對(duì)象上又有一個(gè) constructor 屬性指向構(gòu)造函數(shù)本身。關(guān)系如下圖所示:

核心作用:
原型的本質(zhì)作用就是實(shí)現(xiàn)屬性和方法的共享。原型機(jī)制通過“引用共享”,避免了函數(shù)的重復(fù)創(chuàng)建,提高了內(nèi)存的利用率。
原型鏈
當(dāng)訪問一個(gè)對(duì)象屬性時(shí),JavaScript 引擎會(huì)先查對(duì)象本身,如果本身沒有這個(gè)屬性就會(huì)查它的原型,如果還沒有就繼續(xù)往上找直到找到 null,這條路徑就是原型鏈。
原型鏈上最后一個(gè)真實(shí)存在的對(duì)象是 Object.prototype,它的內(nèi)部屬性 [[Prototype]] 指向 null,即原型鏈的終點(diǎn),因?yàn)榻^大多數(shù)對(duì)象都是從 Object.prototype 繼承而來的。
繼承
傳統(tǒng)面向?qū)ο笳Z言(如 Java/C++)中繼承意味著復(fù)制,派生類會(huì)將基類的屬性和方法硬拷貝一份到自己身上,而 JavaScript 是利用原型鏈機(jī)制模擬了一種“假繼承”。在 new 一個(gè)對(duì)象的時(shí)候,JavaScript 并沒有像傳統(tǒng)類那樣復(fù)制藍(lán)圖上的共享方法,而是通過原型機(jī)制將對(duì)象實(shí)例的原型鏈接到構(gòu)造函數(shù)的 prototype 上。
《You Don’t Know JS》中的觀點(diǎn)是 JS 底層并沒有類的概念,所謂的“繼承”是將兩個(gè)對(duì)象通過 [[Prototype]] 建立的一條單向引用鏈條(也就是原型鏈)。作者認(rèn)為這種行為應(yīng)該稱之為“行為委托”而不是繼承:當(dāng)我們訪問一個(gè)對(duì)象 A 身上不存在的屬性/方法時(shí),它并不是從其他地方復(fù)制或繼承了這個(gè)屬性/方法,而是順著原型鏈找到另一個(gè)對(duì)象 B,并將這個(gè)行為委托給 B 讓它幫執(zhí)行,此時(shí) A 的執(zhí)行上下文(this)依舊綁定在它身上。
但由于普遍的用語習(xí)慣,下面還是將這一委托行為稱為繼承。
JS 實(shí)現(xiàn)繼承的幾種方式:
原型鏈繼承
function Base() {
this.names = ['G.E.M.', 'Gloria'];
}
Base.prototype.say = function() {}
function Derived() {}
Derived.prototype = new Base();
const d1 = new Derived();
const d2 = new Derived();
d1.say(); // 能夠訪問到這是早起最容易想到的辦法,直接將派生類的原型換成基類的實(shí)例。但這樣實(shí)現(xiàn)的一個(gè)痛點(diǎn)就是引用類型的數(shù)據(jù)會(huì)被所有派生類實(shí)例共享,例如此處 d1.names.push('Tang') 會(huì)導(dǎo)致 d2.names 也跟著改變。
構(gòu)造函數(shù)繼承
function Base(name) {
this.name = name;
this.say = function() {console.log(this.name);}
}
function Derived(name, age) {
Base.call(this, name); // 借用基類的構(gòu)造函數(shù)并把 this 綁定到當(dāng)前實(shí)例上
this.age = age;
}這樣實(shí)現(xiàn)繼承會(huì)使得每次實(shí)例化派生類都會(huì)把方法重新創(chuàng)建一遍,沒有實(shí)現(xiàn)函數(shù)引用的復(fù)用,造成內(nèi)存嚴(yán)重浪費(fèi)。
組合機(jī)制
這是將上述二者結(jié)合了起來:
function Base(name) {
this.name = name;
this.names = ['G.E.M.', 'Gloria'];
}
Base.prototype.say = function() {} // 將方法掛在原型上實(shí)現(xiàn)復(fù)用
function Derived(name, age) {
Base.call(this, name); // 拿到獨(dú)立的屬性
this.age = age;
}
Derived.prototype = new Base(); // 將原型鏈拉通
Derived.prototype.constructor = Derived; // 手動(dòng)修復(fù) constructor組合機(jī)制的痛點(diǎn)是調(diào)用了兩次基類的構(gòu)造函數(shù),執(zhí)行 Base.call(this, name) 后派生類實(shí)例就創(chuàng)建了自己獨(dú)立的屬性,但 Derived.prototype = new Base() 讓 Derived.prototype 上也有了 Base 的那份屬性,但這上面的屬性并不會(huì)用到,造成內(nèi)存浪費(fèi)。
寄生組合機(jī)制
這是 ES5 時(shí)代最完美的手寫方案。
function Base(name) {
this.name = name;
}
Base.prototype.say = function() {}
function Derived(name, age) {
Base.call(this, name);
this.age = age;
}
// 最優(yōu)方案是利用 Object.create() 直接建立干凈的原型鏈
// 這個(gè)方法實(shí)現(xiàn)了 Derived.prototype.[[Prototype]] = Base.prototype
Derived.prototype = Object.create(Base.prototype);
Derived.prototype.constructor = Derived;ES6 的 class/extends
class 和 extends 是 ES6 官方推出的基于寄生組合機(jī)制繼承的語法糖。
class Base {
constructor(name) {
this.name = name;
}
say() {}
}
class Derived extends Base {
constructor(name, age) {
super(name); // 這句相當(dāng)于 Base.call(this, name)
this.age = age;
}
}ES5 寄生組合機(jī)制中存在靜態(tài)屬性的丟失,也就是沒有建立起基類和派生類兩個(gè)函數(shù)對(duì)象之間的直接連接,ES6 推出的 extends 補(bǔ)全了這一點(diǎn),底層實(shí)現(xiàn)了 Derived.__proto__ = Base。
到此這篇關(guān)于JavaScript 的原型鏈機(jī)制與繼承詳解的文章就介紹到這了,更多相關(guān)js原型鏈機(jī)制與繼承內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
驗(yàn)證控件與Button的OnClientClick事件詳細(xì)解析
以下就是被我已知忽略的問題和解決方案,當(dāng)我發(fā)覺這個(gè)問題的時(shí)候,冒出了一身冷汗,幸虧做了嚴(yán)格的服務(wù)器端驗(yàn)證,不然可就慘了2013-12-12
fckeditor 獲取文本框值的實(shí)現(xiàn)代碼
獲取文本框值的實(shí)現(xiàn)代碼2009-02-02
javascript作用域鏈與執(zhí)行環(huán)境詳解
這篇文章主要為大家詳細(xì)介紹了javascript作用域鏈與執(zhí)行環(huán)境,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
javascript設(shè)置文本框光標(biāo)的方法實(shí)例小結(jié)
這篇文章主要介紹了javascript設(shè)置文本框光標(biāo)的方法,結(jié)合實(shí)例形式總結(jié)分析了javascript針對(duì)文本框光標(biāo)的位置、設(shè)置及文本操作的相關(guān)技巧,需要的朋友可以參考下2016-11-11
基于JavaScript實(shí)現(xiàn)仿京東圖片輪播效果
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)仿京東圖片輪播效果,代碼簡單易懂,需要的小伙伴參考下吧2015-11-11
JS中圖片緩沖loading技術(shù)的實(shí)例代碼
這篇文章介紹了JS中圖片緩沖loading技術(shù)的實(shí)例代碼,有需要的朋友可以參考一下2013-08-08
微信小程序websocket實(shí)現(xiàn)即時(shí)聊天功能
這篇文章主要為大家詳細(xì)介紹了微信小程序websocket實(shí)現(xiàn)即時(shí)聊天功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05

