ECMAScript语言中没有抽象类和接口的概念,所以并不能像其他语言那样具备继承机制。但ECMAScript可以实现继承,基于原型链来实现:其基本思想就是通过原型继承多个引用类型的属性和方法。 上一篇文章中,已经对原型链进行了分析,对原型链不熟悉的,可以移步上一篇文章:https://juejin.cn/post/7028123009613299743
js实现继承的方式
1. 借用构造函数的方式
function Parent(){
var opt = arguments[0];
this.name = opt.name;
}
function Child(opt){
Parent.call(this,opt)
this.age = opt.age;
}
复制代码
借用构造函数的方式是在子类构造函数中执行父类构造函数的函数体,且将上下文修改为实例创建的新对象,这种方式可以实现给父类构造函数传递参数,但是这种方式的弊端是,必须在构造函数中定义方法和属性,而且子类也不能访问父类原型上的属性和方法,所以这种方式不能单独使用。
2. 基于原型链继承的方式和优缺点
function SuperType() {
this.name = 'super';
this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承 SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors,instance1.name); // "red,blue,green,black" sub
let instance2 = new SubType();
console.log(instance2.colors,instance2.name); // "red,blue,green,black" super
复制代码
通过new SuperType()
生成的SuperType
实例对象变成了SubType的原型对象,那么这个实例对象上的属性和方法,都变成了子类原型对象上的属性和方法,会被所有的子类实例共享的,当子类实例操作时,引用类型的值会被改变,原始类型的值不会被改变。原始值会改变是因为,当我们通过object[name]操作属性时,如果这个对象没有,会给这个对象新增一个该属性,所以不会去修改原型上的同名属性。
优缺点:
- 这种方式不仅继承了父类构造方法和属性,也继承了父类原型对象。
- 缺点一是这种方式父类构造方法中的所有方法和属性都会被子类实例共享,如若子类实例修改引用类型的值会,会影响所有子类实例。
- 缺点二是子类型在实例化时不能给父类型的构造函数传参。
但是我们一般只会继承父类的原型对象,对于构造属性和方法,我们最终希望是生成实例属性和方法,所以我们应该只继承原型。
function SuperType() {
this.name = 'super';
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.flag = true;
function SubType() {}
// 继承 SuperType
SubType.prototype = SuperType.prototype;
SubType.prototype.flag = false;
let instance1 = new SubType();
console.log(instance1.flag); // false
let instance2 = new SuperType();
console.log(instance2.flag); // false
复制代码
通过 SubType.prototype = SuperType.prototype
,我们只继承了父类的原型对象,但是这样也会产生一个问题,子类和父类的原型指向同一个引用,当子类对原型进行操作时,会同时修改了父类的原型。继承不应该向上影响,所以为了解决这个问题,我们需要一个中间对象来做一个缓冲:
// 优化这种继承方式,我们可以使用中间对象来做一个缓冲的方式来解决
function Super(){
this.Mskill = 'Java';
}
Super.prototype = {
name : 'super',
age : 100
}
function Buffer(){};
Buffer.prototype = Super.prototype;
var buffer = new Buffer();
Child.prototype = new Buffer();
function Child(){
this.skill = 'js';
}
复制代码
这样就将子类的原型和父类的原型分隔开来了,我们对子类的原型对象进行操作,不会影响父类的原型,而且也继承了父类的原型。为了解决重写原型导致constructor丢失的问题,我们也需要将子类的constructor属性重新只会子类构造函数,可以自定义一个指向父类构造的属性。
这种方式的企业级封装方案:
// 继承的优化写法,通过封装继承的方法,实现任意两个类之间的继承
function inherit(Target,Origin){
var Buffer = function(){};
Buffer.prototype = new Origin();
var buffer = new Buffer();
Target.prototype = buffer;
Target.prototype.constructor = Target;
Target.prototype.super_class = Origin;
}
复制代码
3.寄生式组合方式
function Parent(){
var opt = arguments[0];
this.name = opt.name;
}
function Child(opt){
Parent.call(this,opt)
this.age = opt.age;
}
inherit(Child,Parent);
复制代码
这种方案是最优的解决方案了,不仅实现了原型链的继承,而且通过借用构造函数的方法,将父类的构造方法和属性,生成到了子类实例上,还能给父类的构造函数传递参数。
4. Object.create方法
上述方案中,我们是创建一个缓冲对象来隔离父类原型和子类原型,创建缓冲对象是通过创建一个空的构造函数,将这个构造函数的原型执行父类原型,然后实例化这个空的构造函数来实现的。但我们有现成的方法来帮助我们完成这一操作,Object.create
方法,参数1接收一个对象,作为生成对象的原型。所以我们可以通过这个方法来改写上述的继承方案:
function inherit(Target,Origin){
var _prototype = Object.create(Origin.prototype); //创建对象
_prototype.constructor = Target; //增强对象
_prototype.super_class = Origin;
Target.prototype = _prototype; //赋值实现继承
}
复制代码
至此,js的继承方式我们已经全部总结了一遍,寄生式组合方式是引用类型继承的最佳方案。