繼承目的:不重復寫類的相同屬性和方法
摘自JavaScript高級程序設計:
繼承是OO語言中的一個最為人津津樂道的概念。許多OO語言都支持兩種繼承方式: 接口繼承 和 實現(xiàn)繼承 。接口繼承只繼承方法簽名,而實現(xiàn)繼承則繼承實際的方法。由于
js中方法沒有簽名,在ECMAScript中無法實現(xiàn)接口繼承.ECMAScript只支持實現(xiàn)繼承,而且其實現(xiàn)繼承主要是依靠原型鏈來實現(xiàn)的。
原型鏈的問題
原型鏈并非十分完美,它包含如下兩個問題。
問題一: 當原型鏈中包含引用類型值的原型時,該引用類型值會被所有實例共享;
問題二: 在創(chuàng)建子類型(例如創(chuàng)建Son的實例)時,不能向超類型(例如Father)的構(gòu)造函數(shù)中傳遞參數(shù)。
有鑒于此, 實踐中很少會單獨使用原型鏈。
為此,下面將有一些嘗試以彌補原型鏈的不足。
常見的幾種繼承方式
在開始之前,為了方面后面使用,我們創(chuàng)建一個 Box 類,作為后面被繼承的父類。
function Box(_a) {
this.a = _a;
this.play();
// 靜態(tài)屬性
Box.static = "static";
}
Box.prototype.aa = 10;
Box.prototype.play = function () {
// 這里的this指向?qū)嵗瘜ο? console.log("play")
}
let b = new Box(1);
console.log(b)
/* 打印結(jié)果
{
a: 1,
__proto__: {
aa: 10,
play: ? (),
constructor: ? Ball(_a),
__proto__: Object
}
}
*/
一、冒充式繼承(借用構(gòu)造函數(shù)繼承)
做法:在子類構(gòu)造函數(shù)的內(nèi)部調(diào)用父類構(gòu)造函數(shù)
-
優(yōu)點:一舉解決了原型鏈的兩大問題:
- 其一, 保證了原型鏈中引用類型值的獨立,不再被所有實例共享;
- 其二, 子類型創(chuàng)建時也能夠向父類型傳遞參數(shù).
缺點:這樣父類會丟失傳入的參數(shù); 還會讓父類構(gòu)造函數(shù)重復執(zhí)行,其中的方法也會重復執(zhí)行(如果不
new Ball,也會執(zhí)行一次play())
function Ball(_a) {
Box.call(this, _a);
}
let b1 = new Ball(20); // TypeError: this.play is not a function
console.log(b1);
console.log(b1.aa); // ===> undefined
/* 注釋Box中的this.play()后打印
{
a: 20,
__proto__: {
constructor: ? Ball(_a)
__proto__: Object
}
}
*/
可見,b1的原型鏈上沒有找到 play() 這個方法,因此報錯。
二、組合式繼承
將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮兩者之長的一種繼承模式。把子類的 prototype 屬性,指向?qū)嵗母割悺?/p>
- 做法:使用原型鏈實現(xiàn)對原型屬性和方法的繼承,通過借用構(gòu)造函數(shù)來實現(xiàn)對實例屬性的繼承。
- 優(yōu)點:組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷
-
缺點:這樣父類會丟失傳入的參數(shù); 還會讓父類構(gòu)造函數(shù)重復執(zhí)行,其中的方法也會重復執(zhí)行(如果不
new Ball,也會執(zhí)行一次play())
function Ball(_a) {
Box.call(this, _a);
}
Ball.prototype = new Box();
// 原型鏈上沒有constructor,需要加上
Ball.prototype.constructor = Ball;
var b2 = new Ball(10);
console.log(b2);
/*
{
a: 10,
__proto__: Box{
a: undefined, // 不是我們所需要的
constructor: ? Ball(_a),
__proto__: {
aa: 10,
play: ?(),
constructor: ? Box(_a),
__proto__: Object
}
}
}
*/
這里有一個需要明確的點:為什么要
new Box(),而不是Box()或者Box?
Box():這樣會直接執(zhí)行Box(),this指向window,而window下沒有play()這個方法,并不能執(zhí)行。Ball.prototype = Box(); // TypeError: this.play is not a function
Box: 在Box中剛開始打印this可以發(fā)現(xiàn),實例化的Ball的原型是Box(這似乎滿足我們的需求)。但是aa和play()并不在原型鏈上,而在Box的prototyoe上,并不能直接訪問到。Ball.prototype = Box; // TypeError: this.play is not a function
三、原型式繼承
- 做法:創(chuàng)建一個臨時性的構(gòu)造函數(shù),然后將父類的原型對象作為這個構(gòu)造函數(shù)的原型,最后將臨時類的一個新實例賦值給子類的原型。
-
優(yōu)點:解決了組合式繼承執(zhí)行兩次
constructor的問題。 -
缺點:
中間類.prototype = 父類.prototype這個過程相當于做了一次淺復制,父類上的引用類型的屬性值,會被實例化的子類修改。
function Ball(_a) {
Box.call(this, _a);
}
function F() { }
F.prototype = Box.prototype;
Ball.prototype = new F();
Ball.prototype.constructor = Ball;
let b3 = new Ball(10);
console.log(b3);
/*
{
a: 10,
__proto__: Box {
constructor: ? Ball(_a)
__proto__:{
aa: 10,
play: ? (),
constructor: ? Box(_a),
__proto__: Object
}
}
}
*/
缺點 - 引用類型被修改的例子:
Box.prototype.arr = [1, 2, 3];
b3.arr.push(4);
let b4 = new Ball(30);
b4.arr.push(5);
console.log(Box.prototype.arr); // [1, 2, 3, 4, 5]
這里有一個需要明確的點:為什么不直接
Ball.prototype = Box.prototype;?因為這樣不是真正意義上的繼承,
Ball與Box二者在原型鏈上沒有了任何聯(lián)系。驗證:
Ball.prototype = Box.prototype; Ball.prototype.constructor = Ball; let b5 = new Ball(10); console.log(b5); /* { a: 10, __proto__: { aa: 10, play: ? (), constructor: ? Ball(_a), __proto__: Object } } */
四、寄生式繼承(常用!推薦!)
做法:創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部來實現(xiàn)類的繼承。
優(yōu)點:寄生組合式繼承、集寄生式繼承和組合繼承的優(yōu)點于一身,是ES5實現(xiàn)基于類型繼承的最有效方法。
// 參數(shù): sub: 子類 sup: 父類
function extend(sub, sup) {
// 創(chuàng)建一個中間類
function F() { }
// 將父類的原型賦值給這個中間替代類
F.prototype = sup.prototype;
// 將原子類的原型保存
let subProto = sub.prototype;
// 將子類的原型設置為中間替代類的實例對象
sub.prototype = new F();
// 將原子類的原型復制到子類原型上,合并超類原型和子類原型的屬性方法
Object.assign(sub.prototype, subProto);
// 設置子類的構(gòu)造函數(shù)時自身的構(gòu)造函數(shù),以防止因為設置原型而覆蓋構(gòu)造函數(shù)
sub.prototype.constructor = sub;
// 給子類的原型中添加一個屬性,可以快捷的調(diào)用到父類的原型方法(目的只是為了讓子類剛方便訪問父類的屬性和方法,類似于ES6的super())
sub.prototype.sup = sup.prototype;
// 如果父類的原型構(gòu)造函數(shù)指向的不是父類構(gòu)造函數(shù),重新指向
if (sup.prototype.constructor !== sup) {
sup.prototype.constructor = sup;
}
}
/* ===== 使用 extend() ===== */
function Ball(_a) {
this.sup.constructor.call(this, _a);
}
extend(Ball, Box);
let b6 = new Ball(10);
console.log(b6);
/*
{
a: 10,
__proto__: Box {
constructor: ? Ball(_a),
sup: {aa: 10, play: ?, constructor: ?}
__proto__: {
aa: 10,
play: ? (),
constructor: ? Box(_a),
__proto__: Object
}
}
}
*/
4.1 如果需要改寫父類的方法,可以對同名方法進行覆蓋
Ball.prototype.play = function () {
this.sup.play.call(this);// 如果需要給父類的方法增加內(nèi)容,則先執(zhí)行父類的同名方法
console.log("end");
}
4.2 Object.assign(sub.prototype, subProto) 的進階寫法
我們知道,constructor應該是不可枚舉的,而使用上面的constructor是可枚舉的,所以這一行代碼,我們可以通過下面的方式進行改寫。
var names = Object.getOwnPropertyNames(subProto);
for (var i = 0; i < names.length; i++) {
var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
Object.defineProperty(sub.prototype, names[i], desc);
}
4.3 也可以封裝到Function上,只需要把sub換成this就可以了
Function.prototype.extend = function (sup) {
function F() { }
F.prototype = sup.prototype;
let subProto = this.prototype;
this.prototype = new F();
var names = Object.getOwnPropertyNames(subProto);
for (var i = 0; i < names.length; i++) {
var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
Object.defineProperty(this.prototype, names[i], desc);
}
this.prototype.constructor = this;
this.prototype.sup = sup.prototype;
if (sup.prototype.constructor !== sup) {
sup.prototype.constructor = sup;
}
}
應用:
function Ball(_a) { this.sup.constructor.call(this, _a); } Ball.prototype.play = function () { this.sup.play.call(this); console.log("end"); } Ball.extend(Box); let b7 = new Ball(10); console.log(b7);
參考資料:
JavaScript中的繼承 - MDN: https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance
JS原型鏈與繼承別再被問倒了:https://juejin.im/post/58f94c9bb123db411953691b#heading-0
