JS五种继承方法和优缺点

2022-07-23 14:26:37

虽然ES6的Class继承确实很方便,但是ES5的继承还是要好好了解一下:

参考视频:详解JS继承(超级详细且附实例)

预备知识

构造函数的属性

functionA(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方法。
  • 缺点:
    • 创建子类实例时,无法传父类参数
    • 子类实例共享
    • 继承单一,无法实现多继承
functionParent(name){this.name= name||'父亲';实例基本属性(该属性,强调私有,不共享)this.arr=[1];//实例引用属性(该属性,强调私用,不共享)}
Parent.prototype.say=function(){//将需要复用、共享的方法定义在父类原型上
	console.log('hello');}functionChild(like){this.like= like;}
Child.prototype=newParent();//核心,但此时Child.prototype.constructor == Parent;
Child.prototype.constructor= Child;//修正constructor指向let boy1=newChild();let boy2=newChild();//优点:共享父类构造函数的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继承多个父类)
  • 缺点:
    • 父类方法不能复用
      由于方法在父构造函数中定义,导致方法不能复用(每次创建子类实例都要创建一遍方法)
    • 子类实例继承不了父类原型上的属性,因为没有用到原型
functionParent(name){this.name= name;//实例基本属性(该属性,强调私有,不共享)this.arr=[1];(该属性,强调私有)this.say=function(){//实例引用属性(该属性,强调复用,需要共享)
		console.log('hello);}}functionChild(name,like){
	Parent.call(this,name);//核心,拷贝了父类的实例属性和方法this.like= like;}let boy1=newChild('小刚','apple');let boy2=newChild('小明','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构造函数,但是因为没有传参,导致子类实例对象的_protoproto _中,一部分属性为undefined

注意name:undefined
functionParent(name){this.name= name;//实例基本属性(该属性,强调私有,不共享)this.arr=[1];//实例引用属性(该属性,强调私用,不共享)}
Parent.prototype.say=function(){//将需要复用、共享的方法定义在父类原型上
	console.log('hello');}functionChild(name,like){
	Parent.call(this,name);//核心,第二次this.like= like;}
Child.prototype=newParent();//核心,第一次
Child.prototype.constructor= Child;//修正constructor指向let boy1=newChild('小刚','apple');let boy2=newChild('小明','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 消失了,改进成功
functionParent(name){this.name= name;//实例基本属性(该属性,强调私有,不共享)this.arr=[1];//实例引用属性(该属性,强调私用,不共享)}
Parent.prototype.say=function(){//将需要复用、共享的方法定义在父类原型上
	console.log('hello');}functionChild(name,like){
	Parent.call(this,name);//核心this.like= like;}
Child.prototype= Parent.prototype//核心,子类原型和父类原型,实际上是同一个
Child.prototype.constructor= Child;//修复代码let boy1=newChild('小刚','apple');let boy2=newChild('小明','orange');let p1=newParent('小爸爸');//优点不演示//缺点1:当修复子类构造函数的指向后,父类实例的构造函数指向也会跟着变了

console.log(boy1.constructor);//没修复之前:Parent
console.log(boy1.constructor,p1.constructor);//修复之后:Child,Child 这就是问题所在

寄生组合继承

完美的继承方案

functionParent(name){this.name= name;//实例基本属性(该属性,强调私有,不共享)this.arr=[1];//实例引用属性(该属性,强调私用,不共享)}
Parent.prototype.say=function(){//将需要复用、共享的方法定义在父类原型上
	console.log('hello');}functionChild(name,like){
	Parent.call(this,name);//核心this.like= like;}//核心 通过创建中间对象,子类原型和父类原型就会隔离开,不再是同一个,有效避免了方式4的缺点
Child.prototype= Object.create(Parent.prototype);

Child.prototype.constructor= Child;//修复代码let boy1=newChild('小刚','apple');let boy2=newChild('小明','orange');let p1=newParent('小爸爸');

console.log(boy1.constructor,p1.constructor);//修复之后:Child,Parent

其中,Object.create()函数等价为:

functionobject(o){functionF(){}F.prototype= o;returnnewF();}

于是中间那段核心代码可改为:

functionobject(o){functionF(){}F.prototype= o;returnnewF();}
	Child.prototype=object(Parent);
	
	Child.prototype.constructor= Child;
  • 作者:DanmoSAMA
  • 原文链接:https://blog.csdn.net/m0_51235736/article/details/113795449
    更新时间:2022-07-23 14:26:37