JavaScript 继承实现的几种方式

前言

继承,作为复用代码的一种有效手段,在面向对象编程中有着重要意义。但是这门脚本语言的确不像某些静态语言那样提供了真正意义上的基于类实现的继承方式,而是采用了一种基于原型的继承。这里将说说在ES5时,使用JavaScript来实现继承的几种方式。
在具体讲这些方式之前,先预先说清楚几个概念。

函数:在JavaScript中,通常每一个函数上都会有一个prototype对象,假如我们通过new这个操作符来使用函数,这时函数的定位更像一个构造器,相当于类那样,提供了一个模板,而此时的prototype对象就是来描述这个模板的。

原型链:当我们尝试去访问一个实例的属性或者方法时。假如当前实例没有,那么会委托到他的原型对象上。假如还是没有,会一直顺着原型链向上找,直到Object.prototype为止。现在主流的浏览器都提供了__proto__来查找他的原型对象。或者也可以使用Object.getPrototypeOf来获取。

1). 原型链继承。

// 基于原型链继承
function Parent(name){
    this.name = name;
}

Parent.prototype.getName = function(){
    return this.name;
}

Parent.prototype.foo = ["parent"];



function Child(){
}

Child.prototype = new Parent();

const parent = new Parent('parent')
const child = new Child('child');
parent.foo.push('hello');
console.log(parent.foo);  // [ 'parent', 'hello' ]
console.log(child.foo); // [ 'parent', 'hello' ]

console.log(child.name); // undefined

console.log(parent instanceof Parent); //true
console.log(child instanceof Parent);  //true
console.log(child instanceof Child);  //true


这段代码可以说明几个问题。

  1. 假如原型对象上存在引用属性,任何一个实例只要改变了这个引用指向数据结构的内容,另外一个实例在访问时也会受到影响,当然大多数时候这不是我们需要的,我们只是希望每一个实例也有同样的属性而已。
  2. 当我们实例化Child时,打印出的name是undefined,当然原因是Child这个构造器原本就没有使用到name属性,但我们希望的是,能够复用到Parent这个构造器,而不是在子类构造器中再写一遍。
  3. 此时,child 实例已经属于 Parent这个“父类”,因为他们之间通过原型链“串”了起来。最后的instanceof 判断相当于以下代码
console.log(parent.__proto__ == Parent.prototype);
console.log(child.__proto__.__proto__ == Parent.prototype);
console.log(child.__proto__ == Child.prototype);

2) 在子类的构造器中调用父类构造器的方法。

// 借用父类构造器函数
function Parent(name,age){
    this.name = name;
    this.age = age;
    this.role = 'parent';
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(){
    const args = Array.prototype.slice.call(arguments)
    Parent.apply(this,args)
    this.role = 'child';
}

const parent = new Parent('p',50);
const child = new Child('c',20);

console.log(parent.name); // p
console.log(parent.age); //50
console.log(parent.getName()); // 50

console.log(child.name); //c
console.log(child.age); //20
console.log(child.getName && child.getName()); // undefined

console.log(parent instanceof Parent); //true
console.log(child instanceof Parent); // false
console.log(child instanceof Child); // true
  1. 首先现在每一个实例都有独立的属性,并且Child的构造方法内调用了父类构造器,能够复用到部分代码
  2. 在子类的实例想要使用getName这个方法时,实际上是找不到的。因为此时这个方法在Parent这个构造器的原型上。当然,也可以把这个方法的声明放到构造器上,或者在Child这个原型上再申请一次,不过这样就又一次陷入了代码冗余的怪圈中。
  3. 此时child instanceof Parent 返回false,我们依旧希望他返回true

3) 将原型链继承和借用父类构造器结合起来,组合式继承

// 借用父类构造器函数
function Parent(name,age){
    this.name = name;
    this.age = age;
    this.role = 'parent';
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(){
    const args = Array.prototype.slice.call(arguments)
    Parent.apply(this,args)
    this.role = 'child';
}

Child.prototype = new Parent();

const parent = new Parent('p',50);
const child = new Child('c',20);

console.log(parent.name); //p
console.log(parent.age); //50
console.log(parent.getName()); //p
console.log(parent.role); //parent


console.log(child.name); //c
console.log(child.age); //20
console.log(child.getName && child.getName()); //c
console.log(child.role); //child

console.log(parent instanceof Parent); true
console.log(child instanceof Parent); // true
console.log(child instanceof Child); // true
  1. 此时,我们发现这样的继承方式已经解决了上述提到的很多问题,有那么点意思了
  2. 但是仔细看就会发现,我们在实现这种继承时,父类的构造器被调用了2次。如果父类的构造器函数十分复杂,那这样的操作也不是我们想要的。

4)原型式继承

// 原型式继承
const foo = {
    name:'xxx',
    age:28,
    getName(){
        return this.name;
    }
};


const clone = Object.create(foo);

console.log(clone.name); // xxx
console.log(clone.age); // 28
console.log(clone.getName()); // xxx
 

这个要区分开和原型链继承的区别。

  1. 可以看到,之前的继承都是首先要有一个父类的构造器,然后子类再去想方设法继承他,实现代码的复用。而在这里,我们通过Object.create这个API,也能实现代码复用。这个API的实现相当于以下的clone函数。
function clone(obj){
    var F = function(){};
    F.prototype= obj;
    return new F();
}
  1. 这里与其说是“继承”,不如说是克隆来的贴切。我们直接返回了一个匿名构造器的实例,这个实例的原型对象指向了一个目标对象,这样一来,当我们去访问这个实例的某个属性时,就会去委托我们指定的这个对象。
  2. 从clone函数实现来看,假如我们传入的obj中有引用类型的属性,多个实例将会共享他,也会有互相影响的问题。

5)寄生继承
原型式继承本质上还是利用了对象的浅复制来实现代码的复用,但是我们的目标对象假如存在引用属性,克隆后的对象都能访问到这一属性,假如我们希望每一个克隆的对象希望有自己的属性可以这么做。

// 寄生式继承
const foo = {
    name:'xxx',
    age:28,
    getName(){
        return this.name;
    }
};

function cloneDecorator(obj){
    const clone = Object.create(obj);
    clone.selfAttributes = ['left','right'];
    clone.getSelfAttrs = function(){
        return this.selfAttributes;
    }
    return clone;
}

const obj1 = cloneDecorator(foo);
const obj2 = cloneDecorator(foo);
obj1.selfAttributes.push(1);
obj2.selfAttributes.push(2);
console.log(obj1.getName()); // xxx
console.log(obj1.getSelfAttrs()); // [ 'left', 'right', 1 ]
console.log(obj2.getName()); // xxx
console.log(obj2.getSelfAttrs()); // [ 'left', 'right', 2 ]

  1. cloneDecorator这个方法实际上有点装饰器模式的味道,我们除了返回这个匿名构造器的实例之外,还对他额外做了一些属性增强,当然,这里又回到了之前碰到的问题,getSelfAttrs这个方法在每一个实例中都声明了一次。但如果放到foo这个对象中,又会产生多个实例互相影响的问题。
  2. 不过原型式继承和寄生式继承提供了一种思路,我们想要把原型链串起来,不一定非得去调用父类的构造器,我们可以直接浅复制父类构造器的原型。这就引出了下面一种继承的实现方式。

6)寄生组合式继承。
将上面提到的3和5结合起来,我们可以写出以下代码

// 借用父类构造器函数
function Parent(name,age){
    this.name = name;
    this.age = age;
    this.role = 'parent';
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(){
    const args = Array.prototype.slice.call(arguments)
    Parent.apply(this,args)
    this.role = 'child';
}

function inherit(subType,superType) {
    const subTypePrototype = Object.create(superType.prototype);
    subTypePrototype.constructor = subType;
    subType.prototype = subTypePrototype;
}

inherit(Child,Parent)

const parent = new Parent('p',50);
const child = new Child('c',20);

console.log(parent.name);
console.log(parent.age);
console.log(parent.getName());
console.log(parent.role);


console.log(child.name);
console.log(child.age);
console.log(child.getName && child.getName());
console.log(child.role);

console.log(parent instanceof Parent);
console.log(child instanceof Parent);
console.log(child instanceof Child);

可以看看打印结果和组合式继承是一样的,inherit函数替代了原来的new操作。这样的继承方式相对来说整合了各种继承的优点。

谈谈class的继承

在ES6中,提供了class这样的语法糖,注意只是语法糖,JavaScript是基于原型来实现继承的。我们可以在babel上来看看,一个class的extends做了什么。

在这里插入图片描述
可见一斑不是么。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值