搞透JS中的继承
使用原型链实现继承
在 JS 中并不存在类,class
只是语法糖,本质还是函数。所以在JS中的继承方式中,本质上就是在重写原型对象。
继承实现的本质就是重写原型对象
我们先来看一下ES5中的继承方式。
function SuperType(){
this.colors = ['red','blue','green']
}
function SubType(){
// 省略
}
// 此处就是在重写子类的原型对象
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('aaa');
console.log(instance1.colors)//['red','blue','green','aaa']
var instance2 = new SubType()
console.log(instance2.colors)//['red','blue','green','aaa']
这种实现方式存在以下问题:
问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
注意:Child.prototype = new Parent()
这段代码重写了Child的constructor属性,Child.prototype.constructor === Parent; //true
所以如果使用var child2 = new Child()
的话,child2的构造函数也不再是Child,而是Parent,也就是说此时child2.constructor === Parent;//true child2.constructor=== Child; //false
(这里的实例child2不是函数,而是对象,因此没有prototype属性)
借用构造函数实现继承
function SuperType(){
this.colors = ['red','blue','green']
}
function SubType(){
//继承了SuperType,使得以后使用new SubType()创造出来的示例会调用SuperType()
SuperType.call(this)
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('aaa');
console.log(instance1.colors)//['red','blue','green','aaa']
var instance2 = new SubType()
console.log(instance2.colors)//['red','blue','green']
这种模式还可以向父类传递参数:
function SuperType(name){
this.name = name
}
function SubType(){
//继承了父类的时候,同时还向父类传递了参数
SuperType.call(this,"nike")
this.age = 29
}
var instance = new SubType();
console.log(instance.name); //nike
console.log(instance.age); //29
借用构造函数实现继承存在以下问题: 问题一:每次创建一个 Child 实例对象时候都需要执行一遍 Parent 函数 问题二:函数不可复用。父类中定义的方法,对子类型而言是不可见的. 每个实例都拷贝一份,占用内存大。 这里要特别解释一下这里的函数不可复用 假设SuperType 在构造函数里定义了一个方法:
function SuperType(name){
this.colors=["red","blue","green"];
this.name=name;
this.sayName = function() {
console.log(this.name);
};
}
那么每一次调用new SuperType,就会在实例内部定义一次这个方法——你定义1000个实例,就会定义1000次这个方法;对于color 和name 这两个变量来说,这无可厚非;但实例方法大多数是相同的,所以这里更推荐把方法定义在SuperType.prototype上,这样每个实例构造出来就自动继承这个方法,不用在构造函数里一次次地写。
组合继承
指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
alert(this.name);
};
function Son(name,age){
Father.call(this,name);//继承实例属性,第一次调用Father()
this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5
var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。 问题:组合继承其实调用了两次父类构造函数, 造成了不必要的消耗。一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部.
寄生组合式继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
// 不必为了指定子类型的原型而调用父类型的构造函数,我们所需要的无非就是父类型原型的一个副本而已。
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})
const child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
现在开发场景我们通常都是能使用ES6都是用ES6实现继承了。
Class继承
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class
实现继承的核心在于使用extends
表明继承自哪个父类,并且在子类构造函数中必须调用super
,因为这段代码可以看成Parent.call(this, value)
。
手动现实extend
function extend(subClass,superClass){
var prototype = Object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
总结与面试题
类的继承就是两点:
1.子类调用父类的构造函数(因为子类要获得父类的属性和方法,所以需要这一步),在es5中是通过在子类的构造函数中使用call来调用父类的构造方法,在es6中则是通过super方法实现的)
2.更改子类的原型链(es5中更改原型链的方式是通过Child.prototype = new Parent()等方式,es6中则是通过extends关键字的方式来实现的)
继承相关的面试题
1.手写ES5或者ES6的继承方式并指出其中的优缺点? 见上
2.ES5和ES6在继承的实现上有什么区别?
ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))
ES6的继承机制实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this.通过关键字class定义类, extends关键字实现继承. 子类必须在constructor方法中调用super方法否则创建实例报错. 因为子类没有this对象, 而是使用父类的this, 然后对其进行加工。super关键字指代父类的this, 在子类的构造函数中, 必须先调用super, 然后才能使用this。