JavaScript 中的繼承:ES3、ES5 和 ES6
選擇一種繼承方式
JavaScript 是一門動態語言,動態意味著高靈活性,而這尤其可以體現在繼承上面。JavaScript 中的繼承有很多種實現方式,可以分成下面四類:
- Mixin 模式,即屬性混入,從一個或多個物件中複製屬性到新的物件中
- 方法借用模式,即通過 call 或 apply 實現方法的重用
- 原型模式,使用 Object.create 方法直接以一個物件為原型創造新的物件
- 類模式,實際上是使用建構函式或 ES6 class
前三種有一個共同點,就是沒有“類”的概念,它們在適當的場景下非常有用,不過也因為沒有類,缺失了很多經典物件導向繼承的要素。例如父子物件之間沒有嚴格的傳承關係,即不一定是 is-a 的關係,這決定了無法將它們直接應用在物件導向分析與設計方面,可以說它們並不是真正的繼承,而是介於繼承和組合之間的程式碼複用方案。
而第四種,類式繼承,無論是使用建構函式還是 ES6 加入的 class,都能表達明確的繼承關係,在需要對繼承重度使用的場景下,應該使用類式繼承。接下來,本文討論的都是類式繼承。
有一點需要牢記:繼承是一種強耦合,應該謹慎使用。
如何學習 JavaScript 中的繼承
理解 JavaScript 裡面的類繼承實現方式,我認為最好的方法是——找一門物件導向機制更為完善的語言,去理解其中的繼承。實際上,JavaScript 中以前就有的 new 和 ES6 加入的 class,都是參考自 Java 語言。
不過,這樣的對照學習是有前提條件的,即首先掌握 JavaScript 中的原型、原型鏈和作用域,否則很容易誤解 JavaScript 本質的執行機制。如果已經理解了這些前置知識,就可以探索一下 JavaScript 中的繼承了。
用 ES3 實現繼承
實現要點:
- 利用 Person.call(this) 執行“方法借用”,獲取 Person 的屬性
- 利用一個空函式將 Person.prototype 加入原型鏈
--
function Person(name) {
this.name = name;
}
Person.prototype.printName = function() {
console.log(this.name);
};
function Bob() {
Person.call(this, "Bob");
this.hobby = "Histroy";
}
function inheritProto(Parent, Child) {
var Fn = function() {};
Fn.prototype = Parent.prototype;
Child.prototype = new Fn();
Child.prototype.constructor = Child;
}
inheritProto(Person, Bob);
Bob.prototype.printHobby = function() {
console.log(this.hobby);
};
console.dir(new Bob());
dir 輸出:
Bob
|-- hobby:"Histroy"
|-- name:"Bob"
|-- __proto__:Person
|-- printHobby:ƒ ()
|-- constructor:ƒ Bob()
|-- __proto__:
|-- printName:ƒ ()
|-- constructor:ƒ Person(name)
|-- __proto__:Object
用 ES5 實現繼承
實現要點:
- 利用 Person.call(this) 執行“方法借用”,獲取 Person 的屬性
- 利用 ES5 增加的 Object.create 方法將 Person.prototype 加入原型鏈
--
function Person(name) {
this.name = name;
}
Person.prototype.printName = function() {
console.log(this.name);
};
function Bob() {
Person.call(this, "Bob");
this.hobby = "Histroy";
}
Bob.prototype = Object.create(Person.prototype, {
constructor: {
value: Bob,
enumerable: false,
configurable: true,
writable: true
}
});
Bob.prototype.printHobby = function() {
console.log(this.hobby);
};
console.dir(new Bob());
dir 輸出:
Bob
|-- hobby:"Histroy"
|-- name:"Bob"
|-- __proto__:Person
|-- printHobby:ƒ ()
|-- constructor:ƒ Bob()
|-- __proto__:
|-- printName:ƒ ()
|-- constructor:ƒ Person(name)
|-- __proto__:Object
用 ES6 實現繼承
實現要點:
- 利用 ES6 增加的 class 和 extends 實現比以前更完善的繼承
--
class Person {
constructor(name) {
this.name = name;
}
printName() {
console.log(this.name);
}
}
class Bob extends Person {
constructor() {
super("Bob");
this.hobby = "Histroy";
}
printHobby() {
console.log(this.hobby);
}
}
console.dir(new Bob());
dir 輸出:
Bob
|-- hobby:"Histroy"
|-- name:"Bob"
|-- __proto__:Person
|-- constructor:class Bob
|-- printHobby:ƒ printHobby()
|-- __proto__:
|-- constructor:class Person
|-- printName:ƒ printName()
|-- __proto__:Object
從 class 和 super 看 JavaScript 與 Java 的繼承
編寫程式碼時,ES6 class 帶來的最明顯的兩個便利是:
- 隱藏原型鏈的拼接過程,將程式碼的重點放在型別之間的傳承
- 使用 super 來實現更簡化、更靈活的多型方法
實際上,ES6 圍繞 class 增加了很多新功能,比如繼承這件事情上,與之前不同的是:用 class 實現的繼承,既包括類例項的繼承關係,也包括類本身的繼承關係。這裡的類其實是特殊的 JavaScript 函式,而在 JavaScript 中,函式是物件的子型別,即函式物件,所以也能夠體現出原型繼承。
例如,用前面的程式碼來說明就是:
// 類例項的繼承關係
Bob.prototype.__proto__ === Person.prototype // true
// 類本身的繼承關係
Bob.__proto__ === Person // true
再來看 ES6 中的 super,子類的方法想借助父類的方法完成一部分工作時,super 就可以派上用場了,這是比繼承更為細粒度的程式碼複用,不過耦合性也也變得更強了。實際上 super 也有很多功能,既可以當作函式使用,也可以當作物件使用。將 class 和 super 結合起來看,就可以領會一下 JavaScript 與 Java 在繼承上的異同了。
與 Java 相同或非常類似的是:
- 在子類構造方法中呼叫父類的構造方法。ES6 中,子類的構造器中必須呼叫父類的構造器來完成初始化,子類的例項是基於父類例項的加工。正是因此,父類的所有行為都可以繼承。所以,ES6 中可以繼承原生資料結構的完整功能,在此基礎上定義自己的資料結構。就像 Java 中繼承 HashMap 類,JavaScript 可以繼承 Number、Array 等建構函式。
與 Java 不同的是:
- 在普通方法中,super 可以呼叫的是父類的原型物件上的方法(可以理解為 super 此時指向父類的原型物件);在靜態方法中,super 可以呼叫父類的靜態方法(可以理解為 super 此時指向父類)。而在 Java 中,通過 super 可以訪問父類中被覆蓋的同名變數或者方法,要訪問靜態方法則是通過“類名.方法名”或“物件名.方法名”。
比較後可見,真的是和 Java 非常類似。
結合前面的內容,可以發現從 ES3 到 ES6,JavaScript 中的物件導向部分一直是在向 Java 靠攏的。尤其增加了 class 和 extends 關鍵字之後,靠攏了一大步。但這些並沒有改變 JavaScript 是基於原型這一實質。Java 中的類就像物件的設計圖,每次呼叫 new 建立一個新的物件,就產生一個獨立的物件佔用獨立的記憶體空間;而在 JavaScript,繼承所做工作實際上是在構造原型鏈,所有子類的例項共享的是同一個原型。所以 JavaScript 中呼叫父類的方法實際上是在不同的物件上呼叫同一個方法,即“方法借用”,這種行為實際上是“委託(delegation)”呼叫。
擴充套件閱讀
參考資料
相關文章
- es5繼承和es6類和繼承繼承
- JavaScript原型鏈以及ES3、ES5、ES6實現繼承的不同方式JavaScript原型S3繼承
- ES6中的類繼承和ES5中的繼承模式詳解繼承模式
- JS繼承es5和es6JS繼承
- ES5和ES6中對繼承的實現繼承
- ES5和ES6的類的繼承繼承
- 【JavaScript】ES5/ES6 建立物件與繼承JavaScript物件繼承
- ES6繼承和ES5繼承是完全一樣的麼?繼承
- es6繼承 vs js原生繼承(es5)繼承JS
- JavaScript之ES5的繼承JavaScript繼承
- es5繼承和es6繼承中靜態方法、靜態屬性的差異繼承
- ES5和ES6及繼承機制繼承
- ES6與ES5繼承的解析繼承
- js中的繼承(es5)JS繼承
- es5建構函式,es6類和類的繼承函式繼承
- JavaScript中的原型和繼承JavaScript原型繼承
- es6 class繼承用es5實現繼承
- JavaScript中的繼承和組合JavaScript繼承
- JavaScript中的繼承JavaScript繼承
- 物件導向(ES5與ES6類的繼承解析)物件繼承
- 前端必知必會ES5、ES6的7種繼承前端繼承
- javascript的原型和繼承JavaScript原型繼承
- es5 原型式繼承原型繼承
- Javascript 繼承和克隆JavaScript繼承
- JavaScript原型和繼承JavaScript原型繼承
- JavaScript中的六種繼承JavaScript繼承
- JavaScript中的函式繼承JavaScript函式繼承
- 淺談JavaScript中的繼承JavaScript繼承
- 征服 JavaScript 面試:類繼承和原型繼承的區別JavaScript面試繼承原型
- 面試中聊到的javascript中的繼承面試JavaScript繼承
- JavaScript的繼承JavaScript繼承
- 從本質認識JavaScript的原型繼承和類繼承JavaScript原型繼承
- 徹底搞懂JavaScript中的繼承JavaScript繼承
- JavaScript 中的六種繼承方式JavaScript繼承
- 探索 React 中 es6 的繼承機制React繼承
- 如何用es5實現繼承繼承
- Javascript繼承4:潔淨的繼承者—-原型式繼承JavaScript繼承原型
- JavaScript繼承JavaScript繼承