在 JS 中,主要是使用原型去实现继承的,但是原型继承会有一些问题,为了解决这个问题,使用的方式有了很多个变种。我们就顺着【发现问题-解决问题】的这个主线,来看下面的多种继承方式。不然真的很难记住下面这些。
1. 最基本的原型链
function Parent() {
this.color = ['red', 'green', 'black']
}
function Child() {}
Child.prototype = new Parent()
const child1 = new Child();
child1.color.push('yellow')
console.log(child1.color)
// ['red', 'green', 'black', 'yellow']
const child2 = new Child();
console.log(child2.color)
// ['red', 'green', 'black', 'yellow']
我们通过直接在子类的原型上挂载了父类的一个实例的方式来实现继承。这种方式也伴随这两个问题:
-
原型上的属性被所有实例共有,会被所有子类的实例所影响,比如在上面那段代码中,我们对 child1 的 color 进行 push 操作,也会反应到 child2 中。原因是二者的原型对象是指向一个的。
-
无法对父类的构造函数进行传参。使用这种模式,由于我们在构造子类的时候,就已经把父类的实例构造好了,所以没法对它进行传参。
由于上面两个缺点,我们就开始考虑改进了。我们得想办法去解决上面两个问题。
2. 盗用构造函数
这种方式就解决了上一种方法带来的两个问题,且看下文。
function Parent(name) {
this.name = name;
this.color = ['red', 'green', 'black']
}
function Child(name) {
Parent.call(this, name);
}
const child1 = new Child('mysteryven');
child1.color.push('yellow')
console.log(child1.color)
// ['red', 'green', 'black', 'yellow']
const child2 = new Child('mysteryven');
console.log(child2.color)
// ['red', 'green', 'black']
主要的改动在 Child 内部。本来是把 Parent 挂载到 Child 的原型上,现在是直接在函数内部使用 call 绑定 this 去调用。这样一来,我们原先在父类里的变量就不会被共用了,同时,也可以给父类传递参数,如上面代码的 name 字段。
虽然解决了上面的问题,但又带来了衍生的问题。那就是所有的方法都在整个实例中,没法共用。大家可以看到,Parent this 中的所有变量和方法全都在当前实例中维护了唯一的一份。这就让 Parent 的功能无法被其他地方一起共用。我们希望的是,如果不是像 colors 这样的数据源,对于无关影响的方法,进行重用。(这个度可能要自己把握,在写代码的时候,我们自己应该能够预测到哪些共用了会有问题,哪些不会有问题)
3. 组合继承
组合继承结合了上面两种方式的优点。
function Parent(name) {
this.name = name;
this.color = ['red', 'green', 'black']
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name);
}
Child.prototype = new Parent();
const child1 = new Child('mysteryven');
child1.color.push('yellow')
console.log(child1.sayName()) // mysteryven
const child2 = new Child('mysteryven2');
console.log(child2.color) // ['red', 'green', 'black']
其实,Child 的原型上也有 Parent 的实例属性,不过,由于原型链查找的策略,会先找当前实例,就找到了使用 Parent.call(this, name) 绑定的这个。而想被共用的方法呢,就可以直接挂在 Parent 的原型上。同时,我们调用 instanceOf 方法也能判断出实例的继承关系:
child1 instanceof Child // true
child1 instanceof Parent // true
4. 原型式继承
这种方法其实和第一种一样,只不过,有了更简单的方法。我们不用为创建对象专门写一个构造函数,使用 Object.create 就好了。
const parent = {
color: ['red']
}
const child = Object.create(parent, {
name: {
value: 'mysteryven'
}
})
与此同时,他也有和第一种一样的问题,parent 对象里的属性在子类里都是共用的。不过,再强调一次,这里的关注点变成了对象了。只用对象实现这里的继承。
5. 寄生式组合继承
在看第三点组合继承的时候,我们会发现,父类被实例化了两次,实例也存了两份,一份在子类身上,一份在子类的原型上。现在就来结合 4 来解决这个问题。
function Parent(name) {
this.name = name;
this.color = ['red', 'green', 'black']
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name);
}
基础代码还是一样的,但是后面在赋值原型的时候就不一样了,我们赋值原型的时候不再直接使用 Child.prototype = new Parent()
了,而是下面这个:
function inheritPrototype(Child, Parent) {
const newChildPrototype = Object.create(Parent);
// 这一句只是为了能通过原型找到实例。没有别的实际作用
// 不重写的话,constructor === Parent
newChildPrototype.constructor = Child;
Child.prototype = newChildPrototype;
}
使用此方法,我们就避免了两次调用父类的构造函数。综合起来就是:
function Parent(name) {
this.name = name;
this.color = ['red', 'green', 'black']
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name) {
Parent.call(this, name);
}
function inheritPrototype(Child, Parent) {
const newChildPrototype = Object.create(Parent);
newChildPrototype.constructor = Child;
Child.prototype = newChildPrototype;
}
inheritPrototype(Child, Parent)
Child.prototype.sayHello = function(){
console.log('hi');
}
const child1 = new Child();
console.log(child1)
整个层级关系就是下图这样:
以上,就是用 ES 5 的语法实现继承的总结了。
到了 ES 6,我们就可以使用类了。继承就可以直接使用 extends 关键词来做,省心了许多。
class Parent {
color = ['1']
}
class Child extends Parent {
}
const a = new Child()
a.color.push(1)
const b = new Child()
console.log(b.color) // ['1']
有一点需要我们注意。可能大家都知道,类其实就是一个语法糖,底层还是原型那一套。但看上面的实例中,我们发现两个实例并不共用父类的实例,那是为什么呢?这是因为在类里,实例成员不会在原型上共享,而方法会在实例中共享。
也就是说:
class Child {
a = () => {} // 不会被共享
constructor() {
this.b = () => {} // 不会被共享
}
c() {} // 只有这种写法的方法会被共享
}
那静态方法呢?其实我们的 Child 本身也是一个对象,静态方法就直接定义在这个对象上了。
以上就是 JS 继承方法的汇总,谢谢阅读。