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;
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值