从ES6中的extends讲js原型链与继承

背景

最近在实现一个表单页面复制功能的时候遇到一个问题,本来页面是有创建、编辑功能的,现在要先加一个一键复制的功能,具体流程和编辑一样,name 字段加一个 -copy ,最后提交是用创建的接口。Edit 是继承的 CreateCopy 来继承 Edit,具体代码可以抽象成以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 写法一
class Edit {
setValue(value) {
console.log(value);
}
}

class Copy extends Edit {
setValue(value){
value = `${value}-copy`;
super.setValue(value);
}
}
const copy = new Copy();
copy.setValue('test');
// test-copy

问题

上面的代码最终会输出 test-copy,也就是我想要的结果,但当时手贱,写了一个箭头函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 写法二
class Edit {
setValue = (value) => {
console.log(value);
}
}

class Copy extends Edit {
setValue = (value) => {
value = `${value}-copy`;
super.setValue(value);
}
}
const copy = new Copy();
copy.setValue('test');
// Cannot read property 'call' of undefined

结果报错了,这就奇怪了,难道箭头函数和普通的函数声明还有这个区别吗?拓展一下,还有以下两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 写法三
class Edit {
setValue = (value) => {
console.log(value);
}
}

class Copy extends Edit {
setValue(value) {
value = `${value}-copy`;
super.setValue(value);
}
}
const copy = new Copy();
copy.setValue('test');
// test

// 写法四
class Edit {
setValue(value) {
console.log(value);
}
}

class Copy extends Edit {
setValue = (value) => {
value = `${value}-copy`;
super.setValue(value);
}
}
const copy = new Copy();
copy.setValue('test');
// test-copy

是不是有点晕了,四种写法结果居然出来三种结果,怎么看都觉得差不多啊。

原理

es5 的时候我们了解到继承的几种实现方式,原型链继承寄生式继承组合继承等,具体这篇博客写的很好,继承一个构造函数是要继承两个部分:

  1. 一个是实例属性,实例属性是每个实例都有一份各自互不干扰的属性。我们这么写:

    1
    2
    3
    function Edit() {
    this.name = 'test';
    }

    其实在 ES6 中对应的就是 = 这种写法:

    1
    2
    3
    class Edit {
    name = 'test'
    }
  2. 一个是方法,方法则是共用的,放在 prototype 上,以此来节省内存。我们这么写:

    1
    2
    3
    Edit.prototype.setValue = function() {
    console.log(this.value);
    }

    ES6 中对应的就是声明函数这种写法:

    1
    2
    3
    4
    5
    class Edit {
    setValue() {
    console.log(this.value);
    }
    }

    我们来看一下 babel 编译后 copy 继承做了哪些事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Copy = function (_Edit) {
_inherits(Copy, _Edit);

function Copy() {
_classCallCheck(this, Copy);

return _possibleConstructorReturn(this, (Copy.__proto__ || Object.getPrototypeOf(Copy)).apply(this, arguments));
}

_createClass(Copy, [{
key: 'setValue',
value: function setValue(value) {
value = value + '-copy';
_get(Copy.prototype.__proto__ || Object.getPrototypeOf(Copy.prototype), 'setValue', this).call(this, value);
}
}]);

return Copy;
}(Edit);
  1. 做了一个 _inherits 操作,这个里面做了两件事:
    • Copy.prototype 设置为 Edit.prototype创建的对象, 并设置其constructor属性(不可枚举)
    • Copy.__proto__ 指向 Edit
  2. Copy.__proto__ 也就是 Edit执行 Edit.apply(this, arguments),这不就是经典继承么。
  3. 执行一个 _createClass操作,就是把自己的方法绑定到prototype上。

总结来说就是:

  1. 继承原型链方法
  2. 继承实例属性
  3. 扩展添加的方法

到这里我们解释了两个classextends所做的事情,那为什么创建对象执行方法就成了不同的结果呢?这要从js对象怎么查找属性,也就是原型链有关。当调用一个对象的某个方法时,首先对象会查找本身有没有设置这个属性,也就是这样:

1
2
3
4
5
var obj = {
say: function() {
console.log('haha');
}
}

如果找不到的话,其实每个对象都有一个__proto__属性,指向创建这个对象的构造函数的原型(这里也就是Object.prototype),原型也就是一个对象,也有自己的属性和__proto__,如果原型还找不到,就这样沿着__proto__一直找下去,这就构成了js的原型链。

回到我们上面的四种写法:

  1. 写法一:EditCopy都将setValue设置到原型上,Edit创建的对象可以通过__proto__找到自己的方法,super操作也可以通过(Object.getPrototypeOf(Copy.prototype), 'setValue', this).call(this, value)调用到EditsetValue 方法。
  2. 写法二:子类和父类都是实例属性,子类的setValue覆盖了父类,执行了子类的setValue,然而父类的prototype里并没有setValue属性,报错。
  3. 写法三:子类继承了父类的实例属性,子类的原型里也有相同的属性,然而根据js原型链查找规则,优先使用了实例属性,也就只执行了父类的setValue方法。
  4. 写法四:基本和写法一一样,不过子类先找到了自己的实例方法,没有从原型里查找。

总结

ES6里如果要使用super使用父类的同名方法,父类的方法不能设置为实例方法。

坚持原创技术分享,您的支持将鼓励我继续创作!