继承
前置知识
- 原型链;
this
详解;
原型链继承
缺点:
-
Son
无法复用Parent
的构造器;
-
Son
的实例对象由于没有独属于自身的属性,访问的都是原型上的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如: const arr = []; arr.push('xxx')
)时会修改掉原型上的值,这样就会对其他共享此原型的对象造成影响;
借用构造函数继承
优点:
Son
可以复用Parent
的构造器;
缺点:
- 由于没有使用原型链,
Son
只能继承Parent
构造器中含有的属性或者方法,无法通过原型链去访问Parent.prototype...
的方法;
一般来说借用构造函数继承这种方式很少单独使用。
组合继承
它是结合 原型链继承 + 借用构造函数继承 两种方式的一种新方式。
优点:
- 避免了 原型链继承 中
Son
无法复用Parent
构造器的问题;
- 避免了借用构造函数继承 中
Son
无法通过原型链去访问Parent.prototype
上的方法的问题;
缺点:
-
son
的实例和son.__proto__
上具有重复的属性名和方法名,这是由于Son.prototype = new Parent()
和Son
中调用了Parent.call(this, xxx)
造成的;
原型式继承
这和ES5
中Object.create()
很类似。
参考:Object.create()
Object.create(proto, [propertiesObject])
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
第一个参数proto
指的就是这个新对象的__proto__
需要指向的对象。
第二个参数是[propertiesObject]
,它是可选的,该参数和Object.defineProperties()
的第二个参数相同。
同时注意,如果proto
参数是原始值类型(null
除外),则抛出一个 TypeError
异常。
比如:
接下来我们通过原型式继承来实现一下Object.create(proto)
方法,它的思路就是创建一个临时对象,让临时对象的构造函数的prototype
指向我们传入的proto
。
接下来应用一下:
缺点:
- 和原型链继承有同样的问题,
const obj = createObject(proto)
,obj
没有独属于自身的属性,访问的都是proto
的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如: const arr = []; arr.push('xxx')
)时会修改掉proto
的属性,这样就会对其他共享此proto
的对象造成影响;
- 无法
createObject
的过程中对生成的对象进行自定义;
寄生式继承
寄生式继承是与原型式继承差不多的思路,只不过再寄生式继承中会对对对象进行某种增强,比如增加某个方法,比如增加某个属性。
其实这里我们就看的出来,寄生式继承只是将原型式继承进行了一层封装而已。
它相比于原型式继承多了一个优点,那就是能够对生成的对象进行自定义。
它同样具有原型式继承的缺点:
- 和原型链继承有同样的问题,
const obj = createObject(proto)
,obj
没有独属于自身的属性,访问的都是proto
的属性,导致修改属性(特指引用类型的变量不改变引用的情况,比如: const arr = []; arr.push('xxx')
)时会修改掉proto
的属性,这样就会对其他共享此proto
的对象造成影响;
寄生组合式继承
就是将 寄生式继承 和 组合继承 两种方式进行合并,摒弃它们的缺点,获得它们的优点。
优点:
Son.prototype = new F()
相比于 Son.prototype = new Parent()
:一方面,不用为了实现继承还需要额外去实例化Parent
,另一方面就是会让Son.prototype
上不会多出冗余的属性(重复的属性);
Son
可以复用Parent
构造函数;
Son
的实例可以通过Parent
的原型去访问对应的属性或者方法;
这里我们在对其进行一些封装:
ES6 Class 继承
ES6
中继承达到的效果其实和我们前面讲的寄生组合式继承类似,同样都是可以继承静态属性方法和实例属性方法,但是他们有本质性的区别。
ES6
规定,子类必须在constructor()
方法中调用super()
,否则就会报错。
这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。
如果不调用super()
方法,子类就得不到自己的this
对象。
为什么子类的构造函数,一定要调用super()
?原因就在于 ES6
的继承机制,与 ES5
完全不同。
比如我们前面封装的_extends
函数(寄生组合式继承),它的本质就是将Son.prototype
和Parent.prototype
关联起来,然后将Son.__proto__
和Parent
关联起来,前者达成实例属性的继承,后者达成静态属性的继承,然后将Parent
保留Son.prototype.super
上,方便我们在Son
中复用父类的构造函数,然后Son
在添加自己的属性或者方法。
然后结合new
的原理,其实这里只创建了一个this
,那就是专属于Son
的this
,这里你会发现,其实调用super
的原因就是为了复用父类的构造函数,简化代码。
因此super
实际上你调与不调都可以,只不过如果不调用super
的话你想拥有父类构造函数中的属性要自己手动写罢了;
但是在ES6
中super
是必须要调用的,而且必须在第一行调用,不然会报错:
这是因为ES6
的继承机制,本质上先实例化一个父类的实例对象,然后再将该对象作为子类的实例,也就是说在子类的构造函数中,只有调用super()
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,必须依赖父类实例的产生。
这就是为什么 ES6
的继承必须先调用super()
方法,因为这一步会生成一个继承父类的this
对象,没有这一步就无法继承父类。
这意味着新建子类实例时,父类的构造函数必定会先运行一次。
注意点
这里发现原型链继承,原型式继承,寄生式继承,都会带来同一个问题:
那就是Son
的实例由于基本没有属于自身的属性,因此Son
的实例获取到的属性值基本都是从其原型上拿到的,因此一般对属性的修改都会修改原型上的属性,导致其他共享这一原型的对象同样遭受影响。
以原型链继承的例子为例:
由于这里的Son
的实例是没有对应的自身的属性,不同的实例获取值时,获取的都是new Parent()
或者Parent.prototype
上的属性或者方法。
然后在设置值时,如果当前实例对象上没有该属性,那么就会在当前实例对象上新增一个属性,而不是去原型链寻找,然后去设置:
请看下面的例子,你是否觉得很奇怪,不是说设置值时不会去寻找对应的原型上的值而后进行设置吗?那下面的例子又作何解释:
这是因为在hobbies.push
方法内部会先通过获取的方式去获取hobbies
的值,而hobbies
在当前实例上并没有,因此获取的就是对应的原型上的值,然后对这个值进行操作。
再结合hobbies
是一个引用类型的值(数组),而push
操作并不会改变这个数组的引用,因此就会影响到另外的实例对hobbies
的访问。
用下面的例子来验证我们的说法:
参考链接
https://es6.ruanyifeng.com/#docs/class#new-target-%E5%B1%9E%E6%80%A7
https://es6.ruanyifeng.com/#docs/class-extends
https://lxchuan12.gitee.io/js-extend/#%E9%9D%A2%E8%AF%95%E5%AE%98%E9%97%AE-js%E7%9A%84%E7%BB%A7%E6%89%BF
JavaScript常用八种继承方案
JS原型链与继承别再被问倒了