JS五種繼承方法和優缺點
技術標籤:JS
雖然ES6的Class繼承確實很方便,但是ES5的繼承還是要好好了解一下:
參考視訊:詳解JS繼承(超級詳細且附例項)
預備知識
建構函式的屬性
function A(name){
this.name = name; //例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; //例項引用屬性(該屬性,強調私用,不共享)
this.say = function(){ //例項引用屬性(該屬性,強調複用,需要共享)
console.log('hello');
}
}
注意:陣列和方法都屬於’例項引用屬性’,但是陣列強調私有不共享,方法需要複用共享。在建構函式中,很少有陣列形式的引用屬性,大部分情況都是:基本屬性+方法。
在建構函式中,為了屬性(例項基本屬性)的私有性、方法(例項引用屬性)的複用共享,提倡:將屬性封裝在建構函式中,將方法定義在原型物件上。
修正constructor指向的意義:任何一個prototype物件都有一個constructor屬性,指向它的建構函式(它本身),更重要的是,每一個例項也有一個constructor屬性,預設呼叫prototype物件的constructor屬性。
在new之後,constructor會指向父類建構函式,如果我們要生成子類建構函式的例項,這些例項的constructor屬性會指向父類建構函式,然而它們是靠子類建構函式生成的,constructor屬性應該指向子類建構函式。因此,不修改constructor指向的話,會導致繼承鏈的紊亂。
(以上來自阮一峰部落格,我目前不清楚繼承鏈紊亂會引起什麼後果,最起碼在我看來,即便不修改constructor指向,好像也沒什麼影響?)
文件的原作者說:要修復constructor指向,原因是:不能判斷子類例項的直接建構函式,到底是子類建構函式還是父類建構函式
JS繼承方式
原型鏈繼承
- 核心:將父類例項作為子類原型
- 優點:方法複用
方法定義在父類的原型上,可以複用父類建構函式的方法,比如say方法。 - 缺點:
- 建立子類例項時,無法傳父類引數
- 子類例項共享
- 繼承單一,無法實現多繼承
function Parent(name){
this.name = name || '父親'; 例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; //例項引用屬性(該屬性,強調私用,不共享)
}
Parent.prototype.say = function(){ //將需要複用、共享的方法定義在父類原型上
console.log('hello');
}
function Child(like){
this.like = like;
}
Child.prototype = new Parent(); //核心,但此時Child.prototype.constructor == Parent;
Child.prototype.constructor = Child; //修正constructor指向
let boy1 = new Child();
let boy2 = new Child();
//優點:共享父類建構函式的say方法
console.log(boy1.say(),boy2.say(),boy1.say === boy2.say); //hello,hello,true
//缺點1:不能傳入父類的引數(比如name),只能傳子類有的引數like
console.log(boy1.name,boy2.name,boy1.name === boy); //父親,父親,true
//缺點2:子類例項共享了父類建構函式的引用屬性,比如arr屬性
boy1.arr.push(2);
console.log(boy2.arr);//[1,2];
//修改了boy1的arr屬性,boy2的arr屬性也會變化,
//因為兩個例項的原型上(Child.prototype)有了父類建構函式的例項屬性arr,所以只要修改了boy1.arr,boy2.arr也變化
借用建構函式
- 核心:借用父類建構函式來增強子類例項,等於是複製父類的例項屬性給子類
- 優點:例項之間獨立
- 建立子類例項,可以向父類建構函式傳參
- 子類例項不共享父類建構函式的引用屬性,如arr
- 可實現多繼承(通過多個call或apply繼承多個父類)
- 缺點:
- 父類方法不能複用
由於方法在父建構函式中定義,導致方法不能複用(每次建立子類例項都要建立一遍方法) - 子類例項繼承不了父類原型上的屬性,因為沒有用到原型
- 父類方法不能複用
function Parent(name){
this.name = name; //例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; (該屬性,強調私有)
this.say = function(){ //例項引用屬性(該屬性,強調複用,需要共享)
console.log('hello);
}
}
function Child(name,like){
Parent.call(this,name); //核心,拷貝了父類的例項屬性和方法
this.like = like;
}
let boy1 = new Child('小剛','apple');
let boy2 = new Child('小明','orange');
//優點1:可向父類建構函式傳參
console.log(boy1.name,boy2.name); //小剛,小明
//優點2:不共享父類建構函式的引用屬性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]
//缺點1:方法不能複用
console.log(boy1.say === boy2.say); //false (說明boy1和boy2的say方法獨立,不是共享的)
//缺點2:不能繼承父類原型上的方法
Parent.prototype.walk = function(){
console.log('我會走路');
}
boy1.walk; //undefined(說明例項不能獲得父類原型上的方法)
組合繼承
- 核心:通過呼叫父類建構函式,繼承父類屬性並保留傳參的優點;然後通過將父類例項作為子類原型,實現函式複用。
- 優點:
- 保留方法1的優點:父類的方法定義在原型物件上,可以實現方法複用
- 保留方法2的優點:建立子類例項,可以向父類建構函式傳參;並且不共享父類的引用屬性,如arr
- 缺點:由於呼叫了2次父類的構造方法,會存在一份多餘的父類例項屬性
原因:第一次Parent.call(this)從父類拷貝一份父類例項屬性,作為子類的例項屬性,第二次Child.prototype = new Parent()建立父類例項作為子類原型,(Child.prototype中的父類屬性和方法會被第一次拷貝來的例項屬性遮蔽掉,所以多餘←這句話沒理解)
我的理解是,第二次new Parent的時候也執行了Parent建構函式,但是因為沒有傳參,導致子類例項物件的_ proto 的 proto _中,一部分屬性為undefined
注意name:undefined
function Parent(name){
this.name = name; //例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; //例項引用屬性(該屬性,強調私用,不共享)
}
Parent.prototype.say = function(){ //將需要複用、共享的方法定義在父類原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心,第二次
this.like = like;
}
Child.prototype = new Parent(); //核心,第一次
Child.prototype.constructor = Child; //修正constructor指向
let boy1 = new Child('小剛','apple');
let boy2 = new Child('小明','orange');
//優點1:可以複用父類原型上的方法
console.log(boy1.say === boy2.say); true
//優點2:可以向父類建構函式傳引數,且不共享父類引用屬性
console.log(boy1.name,boy1.like); //小剛,apple
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr); //[1,2],[1]
//缺點:由於呼叫了2次父類的構造方法,會存在一份多餘的父類例項屬性
組合繼承優化
-
核心:通過這種方式,砍掉父類的例項屬性,這樣在呼叫父類的建構函式的時候,就不會初始化兩次例項,避免組合繼承的缺點
-
優點:
- 只調用一次父類建構函式
- 保留組合繼承的優點
-
缺點:修正建構函式的指向之後,父類例項的建構函式指向,同時也發生變化(這是我們不希望的)
具體原因:因為是通過原型來實現繼承的,Child.prototype上面沒有constructor屬性,就會往上找,這樣就找到了Parent.prototype上面的constructor屬性;當修改了子類例項的constructor屬性,所有的constructor的指向都會發生變化。(我覺得這個原因說得不對,constructor屬性指向自身,Child上有constructor屬性,真正原因可能是因為constructor是引用資料型別,所以修改一方才會影響另一方)
之前的name:undefined 消失了,改進成功
function Parent(name){
this.name = name; //例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; //例項引用屬性(該屬性,強調私用,不共享)
}
Parent.prototype.say = function(){ //將需要複用、共享的方法定義在父類原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
Child.prototype = Parent.prototype //核心,子類原型和父類原型,實際上是同一個
Child.prototype.constructor = Child;//修復程式碼
let boy1 = new Child('小剛','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');
//優點不演示
//缺點1:當修復子類建構函式的指向後,父類例項的建構函式指向也會跟著變了
console.log(boy1.constructor);//沒修復之前:Parent
console.log(boy1.constructor,p1.constructor); //修復之後:Child,Child 這就是問題所在
寄生組合繼承
完美的繼承方案
function Parent(name){
this.name = name; //例項基本屬性(該屬性,強調私有,不共享)
this.arr = [1]; //例項引用屬性(該屬性,強調私用,不共享)
}
Parent.prototype.say = function(){ //將需要複用、共享的方法定義在父類原型上
console.log('hello');
}
function Child(name,like){
Parent.call(this,name); //核心
this.like = like;
}
//核心 通過建立中間物件,子類原型和父類原型就會隔離開,不再是同一個,有效避免了方式4的缺點
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;//修復程式碼
let boy1 = new Child('小剛','apple');
let boy2 = new Child('小明','orange');
let p1 = new Parent('小爸爸');
console.log(boy1.constructor,p1.constructor); //修復之後:Child,Parent
其中,Object.create()函式等價為:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
於是中間那段核心程式碼可改為:
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
Child.prototype = object(Parent);
Child.prototype.constructor = Child;