1. 程式人生 > 其它 >JS五種繼承方法和優缺點

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;