一文彻底搞懂es5和es6继承的区别?

499 阅读5分钟

面试题:es5和es6继承的区别?

最近刷到一道面试题是es5和es6继承的区别?

刚看到这道题的时候,我在想es6的继承是可以通过bable 转为es5的,也就是说es6能实现的,es5也都能实现,在好奇的趋势下研究了一下bable如何实现继承的,于是就有了这篇文章。

面试题中所说的ES5继承可能并不完全准确,可能更多的是想表达ES5的寄生组合继承和ES6的继承有哪些不同之处?在比较两者之间的区别之前,我们先看看怎么通过原型实现一个寄生组合继承吧。

原型

如果你懒得看文字,你可以直接看图

Untitled.jpeg

每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象就是原型。

当调用构造函数创一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。

原型是对象的创建者与对象之间的纽带,有了它对象的创建者能够轻而易举的共享信息给所有的实例。

原型链

每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null

如果查找一个属性或者方法,会先在实例上查找,如果不存在则会在原型上查找,以此类推,直到找到或者原型为null

寄生组合式继承

继承无非就是子类的实例可以直接访问父类的方法或者属性,根据原型链的特性,我们构造出下图的关系

Untitled.png

// 父类
function Parent(name) {
		// 属性
    this.name = name;
}
// 父类方法
Parent.prototype.say = function () {
    console.log('hello name:', this.name);
};

function Child(name) {
		/** 关键操作 **/
    Parent.call(this,arguments);
    this.type = 'child';
}
/** 关键操作 **/
// 将原型指定为一个 对象,这个对象的原型指向Parent的原型
Child.prototype = Object.create(Parent.prototype);
// 由于我们覆盖了Child 原来的原型,所以丢失了constructor,我们重新添加一下
Child.prototype.constructor = Child;

// 添加自己的方法
Child.prototype.sayType = function () {
    console.log('hello type:', this.type);
};

这样我们就用ES5实现了一个寄生组合式继承,他包含两个关键点:

1、在Child中调用Parent

2、将Child 的原型指向 以Parent.prototype 为基础创建的副本(这个副本将的原型指向Parent.prototype)

和ES6的比较

在比较之前我们先来一段es6的继承,为了比较完整的对比,我们在Parent中增加了一个静态属性

class Parent {
	constructor(){
		this.name='name'
	}
	say(){
		console.log('hi name:',this.name)
	}
	// 静态方法
	static staticProp = "staticprop"
}

class Child extends Parent{
	constructor(){
		super();
		this.type='type'
	}
	sayType(){
		console.log('hi type:',this.type);
	}
}

const child = new Child();
child.say(); 
child.sayType(); 
// 重点关注
child.staticProp//staticprop

我们发现ES6的Class可以增加静态属性,并且子类会继承父类的静态属性,但是我们的组合式继承并没有考虑到这一点。

es5中增加静态方法

静态方法有一个特点是,只能通过类名来调用,在ES5中,我们只需要把这个属性定义在构造函数上是不是就可以模拟静态方法


function Parent(name) {

    this.name = name;
}
// 父类方法
Parent.prototype.say = function () {
    console.log('hello name:', this.name);
};

// 静态方法
Parent.staticProp = 'staticProp'

function Child(name) {

    Parent.call(this,arguments);
    this.type = 'child';
}

Child.prototype = Object.create(Parent.prototype);

Child.prototype.constructor = Child;

Child.prototype.sayType = function () {
    console.log('hello type:', this.type);
};

Child.staticProp //undefined

通过上述代码,我们发现寄生组合继承 Child并没有继承 Parent的静态属性,

继承静态属性/方法

我们知道在JS中一切应用类型皆为对象,Function也不例外,所以Function同样也包含原型,如果我们把Child的内部属性[[Prototype]] 指向Parent 呢,那么如果Child中查找不到的属性就会在Parent上去查找,这样是不是就实现了静态方法的继承。

我们只需要增加一段代码就可以实现静态方法的继承

Object.setPrototypeOf(Child, Parent);
// 等同于
// Child.__proto__ = Parent;

哒哒~

没错,到这里一切看上去都已经很完美。

然而

有一天来了个需求,需要在原生数组上扩展,第一反应是用原生的Array 继承一个自己的MyArray,然后在这上面增加自己的方法不就好了嘛,于是我写了以下代码

function MyArray() {
		/** 关键操作 **/
    Array.call(this,arguments);
}
/** 关键操作 **/
// 将原型指定为一个 对象,这个对象的原型指向Parent的原型
MyArray.prototype = Object.create(Array.prototype);
// 由于我们覆盖了Child 原来的原型,所以丢失了constructor,我们重新添加一下
MyArray.prototype.constructor = Child;

// 添加自己的方法
MyArray.prototype.sum = function () {
    return this.reduce(function (acc, val) {
        return acc + val;
    }, 0);
};
const myArray = new MyArray();
myArray.push(1);
console.log(myArray.length);//0

咦~,我们刚才写的继承居然对原生数组不起作用

我们来简单分析一下为什么不行

new MyArray 返回的是一个对象,而new Array 返回的是一个数组,数组的特性全部丢失了

于是我们需要做以下改进

  1. 返回数组(父类函数执行的结果)
  2. 改造父类返回值的原型链
function MyArray() {
    const super = Array.call(this,arguments);
		if(super){
			Object.setPrototypeOf(super, MyArray.prototype);
		}
		const _self = super||this;
		return _self;
}
/** 关键操作 **/
// 将原型指定为一个 对象,这个对象的原型指向Parent的原型
MyArray.prototype = Object.create(Array.prototype);
// 由于我们覆盖了Child 原来的原型,所以丢失了constructor,我们重新添加一下
MyArray.prototype.constructor = Child;

// 添加自己的方法
MyArray.prototype.sum = function () {
    return this.reduce(function (acc, val) {
        return acc + val;
    }, 0);
};
const myArray = new MyArray();
myArray.push(1);
console.log(myArray.length);//1

通过上述改造,我们完整的模拟了原生数组的继承

事实上上述方法参照了babel的继承,babel的继承写法上有一些不同,如果感兴趣的同学可以自行研究。

小结

es5 继承和es6继承到底有什么区别?

ES5的的子构造函数的 Sub.__proto__ 指向的是 Function.prototype。 而 ES6 实现的继承,Sub.__proto__ 的会指向 Parent

es5返回的是子类的对象,es6会返回改造后的父类的对象