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

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

