JavaScript原型鏈以及ES3、ES5、ES6實現繼承的不同方式
本文首發於GitHub,《JavaScript原型鏈以及ES3、ES5、ES6實現繼承的不同方式》,歡迎關注我的GitHub。
原型
執行程式碼var o = new Object();
,此時o物件內部會儲存一個指標,這個指標指向了Object.prototype,當執行o.toString()
等方法(或訪問其他屬性)時,o會首先檢視自身有沒有該方法或屬性,如果沒有的話就沿著內部儲存的指標找到Object.prototype
物件,然後檢視Object.prototype
物件是否有對應名稱的方法或屬性,如果有就呼叫Object.prototype
的方法或屬性。我們把這個指標叫做o物件的原型,你可以把它看做是Java類繼承中的super
關鍵字。
ES3規範中定義了Object.prototype.isPrototypeOf()方法,該方法可以判斷某個物件是不是另一個物件的原型。Object.prototype.isPrototypeOf(o)
返回true值可以確定Object.prototype就是o物件的原型。在ES3規範中,不能直接讀取o物件的原型,也就是o物件的原型看不見摸不著的。ES5.1規範定義了Object.getPrototypeOf()方法,通過該方法可以獲取物件的原型。我們可以通過Object.getPrototypeOf(o) === Object.prototype
再次驗證Object.prototype就是o物件的原型。ES6規範更加直接,為物件新增了一個__proto__
屬性,通過這個屬性就可以獲得物件的原型,所以在支援__proto__
的瀏覽器中,o.__proto__ === Object.prototype
也會返回true。
當我們執行var x = new X();
時,瀏覽器會執行x.__proto__ = X.prototype
,會將例項化物件的原型設定為對應的類的prototype物件,這一點很重要。
原型鏈
我們執行如下程式碼:
function Person(){};
var p = new Person();
`p.__proto__`指向了`Person.prototype`,`Person.prototype`的原型是`Person.prototype.__proto__`,其指向了`Object.prototype`,`Object.prototype.__proto__`為null。
通過`__proto__`向上追蹤形成了如下的鏈式結構:
p -> Person.prototype -> Object.prototype -> null
**這一原型的鏈式結構就叫做原型鏈。Object.prototype的原型是null,也就是說Object.prototype沒有原型。**
JavaScript 物件有一個指向一個原型物件的鏈。當試圖訪問一個物件的屬性時,它不僅僅在該物件上搜尋,還會搜尋該物件的原型,以及該物件的原型的原型,依此層層向上搜尋,直到找到一個名字匹配的屬性或到達原型鏈的末尾。
JavaScript中的繼承是通過原型實現的,雖然在ES6中引入了`class`關鍵字,但是它只是原型的語法糖,JavaScript繼承仍然是基於原型實現的。
## ES3實現繼承
在JavaScript中,所謂的類就是函式,函式就是類。一般情況下,我們在函式的prototype上面定義方法,因為這樣所有類的例項都可以公用這些方法;在函式內部(建構函式)中初始化屬性,這樣所有類的例項的屬性都是相互隔離的。
我們定義ClassA和ClassB兩個類,想讓ClassB繼承自ClassA。
ClassA程式碼如下所示:
function ClassA(name, age) {
this.name = name;
this.age = age;
}
ClassA.prototype.sayName = function () {
console.log(this.name);
};
ClassA.prototype.sayAge = function () {
console.log(this.age);
};
ClassA建構函式內部定義了`name`和`age`兩個屬性,並且在其原型上定義了`sayName`和`sayAage`兩個方法。
ClassB如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
ClassB新增了`job`屬性,我們在其建構函式中執行`ClassA.apply(this, [name, age]);`,相當於在Java類的建構函式中通過`super()`呼叫父類的建構函式以初始化相關屬性。
此時我們可以通過`var b = new ClassB(“sunqun”, 28, “developer”);`進行例項化,並可以訪問`b.name`、`b.age`、`b.job`三個屬性,但此時b還不能訪問ClassA中定義的`sayName`和`sayAage`兩個方法。
然後我們新增程式碼`ClassB.prototype = ClassA.prototype;`,此時ClassB的程式碼如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
//新增
ClassB.prototype = ClassA.prototype;
當執行`var b = new ClassB(“sunqun”, 28, “developer”);`時,b.__proto__指向的是ClassB.prototype,由於通過新增的程式碼已經將`ClassB.prototype`指向了`ClassA.prototype`,所以此時b.__proto__指向了`ClassA.prototype`。這樣當執行`b.sayName()`時,會執行`b.__proto__.sayName()`,即最終執行了`ClassA.prototype.sayName()`,這樣ClassB的例項就能呼叫ClassA中方法了。
此時我們想為ClassB新增加一個`sayJob`方法用於輸出`job`屬性的值,如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
ClassB.prototype = ClassA.prototype;
//新增
ClassB.prototype.sayJob = function(){
console.log(this.job);
};
此時問題出現了,我們為`ClassB.prototype`新增`sayJob`方法時,其實修改了`ClassA.prototype`,這樣會導致ClassA所有的例項也都有了`sayJob`方法,這顯然不是我們期望的。
為了解決這個問題,我們再次修改ClassB的程式碼,如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
// ClassB.prototype = ClassA.prototype;
//修改
ClassB.prototype = new ClassA();
ClassB.prototype.constructor = ClassB;
ClassB.prototype.sayJob = function(){
console.log(this.job);
};
我們通過執行`ClassB.prototype = new ClassA();`將ClassA例項化的物件作為ClassB的prototype,這樣ClassB仍然能夠使用ClassA中定義的方法,但是`ClassB.prototype`已經和`ClassA.prototype`完全隔離了。我們的目的達到了,我們可以隨意向`ClassB.prototype`新增我們想要的方法了。有個細節需要注意,`ClassB.prototype = new ClassA();`會導致`ClassB.prototype.constructor`指向ClassA的例項化物件,為此我們通過`ClassB.prototype.constructor = ClassB;`解決這個問題。
一切貌似完美的解決了,但是這種實現還是存在隱患。我們在執行`ClassB.prototype = new ClassA();`的時候,給ClassA傳遞的是空引數,但是ClassA的建構函式預設引數是有值的,可能會在建構函式中對傳入的引數進行各種處理,傳遞空引數很有可能導致報錯(當然本示例中的ClassA不會)。於是我們再次修改ClassB的程式碼如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
//修改
function ClassMiddle() {
}
ClassMiddle.prototype = ClassA.prototype;
ClassB.prototype = new ClassMiddle();
ClassB.prototype.constructor = ClassB;
ClassB.prototype.sayJob = function () {
console.log(this.job);
};
這次我們引入了一個不需要形參的函式`ClassMiddle`作為ClassB和ClassA之間的中間橋樑。
1. `ClassMiddle.prototype = ClassA.prototype;`: 將`ClassMiddle.prototype`指向`ClassA.prototype`,這樣ClassMiddle可以訪問ClassA中定義的方法。
2. `ClassB.prototype = new ClassMiddle();`: 將ClassMiddle的例項化物件賦值給ClassB.prototype,這樣就相當於執行了`ClassB.prototype.__proto__ = ClassMiddle.prototype;`,所以ClassB就能使用ClassMiddle中定義的方法,又因為`ClassMiddle.prototype`指向了`ClassA.prototype`,所以`ClassB.prototype.__proto__`也指向了`ClassA.prototype`,這樣ClassB能使用ClassA中定義的方法。
以上思路的精妙之處在於ClassMiddle是無參的,它起到了ClassB和ClassA之間的中間橋樑的作用。
現在我們為ClassA新增一些靜態屬性和方法,ClassA新增如下程式碼:
...
//為ClassA新增靜態屬性
ClassA.staticValue = "static value";
//為ClassA新增靜態方法
ClassA.getStaticValue = function() {
return ClassA.staticValue;
};
ClassA.setStaticValue = function(value) {
ClassA.staticValue = value;
};
靜態屬性和方法不屬於某一個例項,而是屬於類本身。ClassA.prototype上面定義的方法是例項方法,不是靜態的。靜態屬性和方法是直接新增在ClassA上的。
為了使ClassB也能繼承ClassA的靜態屬性和方法,我們需要為ClassB新增如下程式碼:
...
//ClassB繼承ClassA的靜態屬性和方法
for (var p in ClassA) {
if (ClassA.hasOwnProperty(p)) {
ClassB[p] = ClassA[p];
}
}
我們最終可以將上述繼承程式碼的公共部分抽離成一個extendsClass方法,如下所示:
function extendsClass(Child, Father) {
//繼承父類prototype中定義的例項屬性和方法
function ClassMiddle() {
}
ClassMiddle.prototype = Father.prototype;
Child.prototype = new ClassMiddle();
Child.prototype.constructor = Child;
//繼承父類的靜態屬性和方法
for (var p in Father) {
if (Father.hasOwnProperty(p)) {
Child[p] = Father[p];
}
}
}
我們只需要執行`extendsClass(ClassB, ClassA);`就可以完成大部分繼承的邏輯。
最終ClassA的完整程式碼如下所示:
function ClassA(name, age) {
this.name = name;
this.age = age;
}
ClassA.prototype.sayName = function() {
console.log(this.name);
};
ClassA.prototype.sayAge = function() {
console.log(this.age);
};
ClassA.staticValue = "static value";
ClassA.getStaticValue = function() {
return ClassA.staticValue;
};
ClassA.setStaticValue = function(value) {
ClassA.staticValue = value;
};
ClassB的完整程式碼如下所示:
function ClassB(name, age, job) {
ClassA.apply(this, [name, age]);
this.job = job;
}
extendsClass(ClassB, ClassA);
ClassB.prototype.sayJob = function() {
console.log(this.job);
};
我們可以在控制檯中進行一下簡單測試:
ES5實現繼承
ES5.1規範中新增了Object.create()方法,該方法會傳入一個物件,然後會返回一個物件,返回的物件的原型指向傳入的物件,比如執行程式碼var output = Object.create(input)
,相當於執行程式碼output.__proto__ = input;
,output的原型是input。我們可以簡化之前的程式碼,不再需要ClassMiddle,只需要執行ClassB.prototype = Object.create(ClassA.prototype);
即可,相當於執行程式碼ClassB.prototype.__proto__ = ClassA.prototype;
。
而且ES5.1中新增了Object.keys()方法用以獲取物件自身的屬性陣列,我們可以用該方法簡化繼承父類靜態屬性和方法的過程。
根據以上兩點,我們修改extendsClass方法如下所示:
function extendsClass(Child, Father) {
//繼承父類prototype中定義的例項屬性和方法
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child;
//繼承父類的靜態屬性和方法
Object.keys(Father).forEach(function(key) {
Child[key] = Father[key];
});
}
ClassA和ClassB的程式碼無需變化。
ES6實現繼承
我們之前提到,ES6規範定義了Object.prototype.proto屬性,該屬性既可讀又可寫,通過__proto__
屬性我們可以直接指定物件的原型。於是在ES6中我們將extendsClass修改為如下所示:
function extendsClass(Child, Father) {
//繼承父類prototype中定義的例項屬性和方法
Child.prototype.__proto__ = Father.prototype;//暴力直接,利用__proto__屬性設定物件的原型
//繼承父類的靜態屬性和方法
Child.__proto__ = Father;
}
直接修改物件的__proto__
屬性值不是最佳選擇,ES6規範中還定義了Object.setPrototypeOf()方法,通過執行Object.setPrototypeOf(b, a)
會將a物件作為b物件的原型,即相當於執行了b.__proto__ = a;
。為此我們利用該方法再次精簡我們的extendsClass方法,如下所示:
function extendsClass(Child, Father) {
//繼承父類prototype中定義的例項屬性和方法
Object.setPrototypeOf(Child.prototype, Father.prototype);
//繼承父類的靜態屬性和方法
Object.setPrototypeOf(Child, Father);
}
Object.setPrototypeOf(Child.prototype, Father.prototype);
相當於執行程式碼Child.prototype.__proto__ = Father.prototype;
,使得Child能夠繼承Father中的例項屬性和方法。Object.setPrototypeOf(Child, Father);
相當於執行程式碼Child.__proto__ = Father;
,使得Child能夠繼承Father中的靜態屬性和方法。
ES6中引入了class關鍵字,可以用class
直接定義類,通過extends
關鍵字實現類的繼承,還可以通過static
關鍵字定義類的靜態方法。
我們用class
等關鍵字重新實現ClassA和ClassB的程式碼,如下所示:
class ClassA{
constructor(name, age){
this.name = name;
this.age = age;
}
sayName(){
console.log(this.name);
}
sayAge(){
console.log(this.age);
}
static getStaticValue(){
return ClassA.staticValue;
}
static setStaticValue(value){
ClassA.staticValue = value;
}
}
ClassA.staticValue = "static value";
class ClassB extends ClassA{
constructor(name, age, job){
super(name, age);
this.job = job;
}
sayJob(){
console.log(this.job);
}
}
ES6中不能通過static
定義類的靜態屬性,我們可以直接通過ClassA.staticValue = "static value";
定義類的靜態屬性。
需要注意的是,class
關鍵字只是原型的語法糖,JavaScript繼承仍然是基於原型實現的。
並不是所有的瀏覽器都支援class
關鍵字,在生產環境中,我們可以編寫ES6的程式碼,然後用Babel或TypeScript將其編譯為ES5等主流瀏覽器支援的語法格式。
總結
執行
var x = new X();
時,瀏覽器會執行x.__proto__ = X.prototype
,會將例項化物件的原型設定為對應的類的prototype物件。實現類繼承的關鍵是
Child.prototype.__proto__ = Father.prototype;
,這樣會將Father.prototype
作為Child.prototype
的原型。Object.prototype.__proto__
屬性是在ES6規範中所引入的,為了在ES3和ES5中需要通過各種方式模擬實現對Object.prototype.__proto__
進行賦值。通過執行
Child.__proto__ = Father;
可以實現繼承父類的靜態屬性和方法。
參考
[1] MDN, ES3 Object.prototype.isPrototypeOf()
[2] MDN, ES5.1 Object.create()
[3] MDN, ES5.1 Object.getPrototypeOf()
[4] MDN, ES6 Object.setPrototypeOf()
[5] MDN, ES6 Object.prototype__proto__
[6] MDN, ES6 Classes
相關文章
- JavaScript 中的繼承:ES3、ES5 和 ES6JavaScript繼承S3
- Javascript之繼承(原型鏈方式)JavaScript繼承原型
- 原型鏈實現繼承的6種方式原型繼承
- Javascript 中實現物件原型繼承的三種方式JavaScript物件原型繼承
- javascript原型鏈繼承的使用JavaScript原型繼承
- javascript原型鏈及繼承JavaScript原型繼承
- javascript - 繼承與原型鏈JavaScript繼承原型
- 【機制】JavaScript的原型、原型鏈、繼承JavaScript原型繼承
- 繼承的實現方式及原型概述繼承原型
- js 原型鏈實現類的繼承JS原型繼承
- JavaScript實現繼承的方式JavaScript繼承
- es6 class繼承用es5實現繼承
- 圖解JavaScript原型鏈繼承圖解JavaScript原型繼承
- es5 原型式繼承原型繼承
- ES5和ES6中對繼承的實現繼承
- 【JavaScript】ES5/ES6 建立物件與繼承JavaScript物件繼承
- javascript基礎-原型鏈與繼承JavaScript原型繼承
- 白話JavaScript原型鏈和繼承JavaScript原型繼承
- es5繼承和es6類和繼承繼承
- es6繼承 vs js原生繼承(es5)繼承JS
- [JavaScript]原型、原型鏈、建構函式與繼承JavaScript原型函式繼承
- javascript繼承的實現方式介紹JavaScript繼承
- 深入理解JavaScript原型鏈與繼承JavaScript原型繼承
- 深入淺出JavaScript之原型鏈&繼承JavaScript原型繼承
- JavaScript之ES5的繼承JavaScript繼承
- JS原型鏈繼承JS原型繼承
- javascript的原型和繼承JavaScript原型繼承
- ES6與ES5繼承的解析繼承
- JavaScript原型和繼承JavaScript原型繼承
- 深入淺出JavaScript之原型鏈和繼承JavaScript原型繼承
- 從babel實現es6類的繼承來深入理解js的原型及繼承Babel繼承JS原型
- 如何用es5實現繼承繼承
- JS繼承es5和es6JS繼承
- Javascript繼承4:潔淨的繼承者—-原型式繼承JavaScript繼承原型
- JS的原型鏈和繼承JS原型繼承
- 繼承的實現方式繼承
- javascript實現的繼承的幾種常用方式JavaScript繼承
- 原型,繼承——原型繼承原型繼承