原型链继承
代码实现
function Parent() {
this.name = 'I am Parent'
}
Parent.prototype.run = function () {
console.log(this.name)
}
function Child() {}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const c1 = new Child()
c1.run() // I am Parent
问题分析
上面的代码运行的大致过程为:Child类实例化一个c1对象,接着c1访问Child类的属性run,然而Child类上并没有这个属性,接着会去Child的原型上面去找,如果Child.prototype上也没有,则会去Parent.prototype上面去找,从而实现了继承。
代码分析
- 声明一个Parent类,设置Parent中name属性,并赋值"I am Parent"
- 在Parent.prototype上设置一个run方法,方法体中打印name属性
- 声明一个子类Child
- 将Child.prototype重新赋值为Parent类的实例对象(new Parent()相当于实例化一个新对象,该对象拥有Parent类上的属性以及Parent原型上的属性)。
- 如果Child.prototype = Parent.prototype 这种赋值的话,会导致只能找到Parent原型上面的属性,而Parent上面的属性找不到,比如name属性,运行c1.run会打印出undefined。
- 经过上一步
Child.prototype = new Parent()
, 导致了Child的constructor指向了Parent(c1.proto === Child.prototype, Child.prototype.proto === Parent.prototype), 但是有时候需要根据constructor来进行类型判断,为了防止判断出错,需要将原型上的属性constructor重新指向Child,因此才有了这一步操作Child.prototype.constructor = Child
原型链继承的优缺点
优点:
- 原型上面的方法是共享的,都是指向同一内存地址。 缺点:
- 创建实例的时候,不能传参。
- 如果类中有的属性为引用类型,那么,一旦其中某个实例改变了这个属性,那么其他实例中的该属性也会跟着改变。看下面这个例子
function Parent() {
this.name = 'I am Parent'
this.colors = ['red', 'black']
}
// ... 省略部分代码
const c1 = new Child()
const c2 = new Child()
console.log(c1.colors) // ["red", "black"]
c2.colors.push('yellow')
console.log(c1.colors) // ["red", "black", "yellow"]
构造函数继承
为了解决上面继承方式的缺点,我们来看一下构造函数继承。
代码实现
function Parent(name, colors) {
this.name = name
this.colors = colors
}
function Child(id, name, colors) {
Parent.apply(this, Array.from(arguments).slice(1))
this.id = id
}
const c1 = new Child('c1', 'child1', ['red'])
const c2 = new Child('c2', 'child2', ['red'])
c2.colors.push('yellow')
console.log(c1.colors) // ["red"]
console.log(c2.colors) // ["red", "yellow"]
问题分析
原型链继承中,如果类中有引用类型的属性,那么不同实例之间有可能会互相影响,为了解决该问题,可以把Parent类上面的属性方法都放在Child上面,不放到原型对象上面,防止被所有实例所共享,同时可以使用call、apply等方法改变this指向,同时还能够传参,因此,可以解决原型链继承方式的缺点。
代码分析
- 声明一个Parent类,接收两个参数,name、colors
- 声明一个Child类,接收三个参数,id、name、colors
- 这里使用apply方法改变了this指向,并且传参,复制了一遍Parent上的操作
- 实例化两个变量 c1和c2,打印引用类型的属性colors,两个实例上的属性colors互相不影响了
构造函数继承优缺点
优点:
- 可以传参
- 引用类型的属性在各个实例上面不会互相影响 缺点:
- 如果类中定义了方法的话,那么实例在创建的时候,就会创建一遍方法,导致多开辟了一块内存空间,造成了内存浪费,来看下面的代码
function Parent(name, colors) {
this.name = name
this.colors = colors
this.run = function() {
console.log(this.name)
}
}
// ... 省略部分代码
const c1 = new Child('c1', 'child1', ['red'])
const c2 = new Child('c2', 'child2', ['red'])
cosnole.log(c1.run === c1.run) // false
因为run定义在了Parent类中,为了检验实例c1和实例c2中的run是否指向同一块内存地址,通过c1.run === c1.run
判断是否为true, 结果返回的false,说明每次实例化的时候都会创建一遍run方法。
组合继承
组合继承是谁和谁的组合?
在解答这个问题之前,先回顾一下上面的两种继承方式
- 原型链继承方式,实现了基本继承,方法放在了prototype上面,子类可以直接调用,但是引用类型的属性会被所有实例所共享,而且不能传参
- 构造函数继承方式,解决了原型链继承遇到的两个问题,但是也引出了另外一个问题,就是类中声明的方法在实例化的时候会被重复创建的问题,导致内存占用过多。
通过上面总结的内容,突然发现原型链继承方式可以解决方法重复创建的问题,因此,我们将这两种继承方式组合起来使用,这就叫做组合继承(原型链继承+构造函数继承)
代码实现
function Parent(name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.run = function() {
console.log(this.name)
}
function Child(id, name, colors) {
this.id = id
Parent.apply(this, Array.from(arguments).slice(1))
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
const c1 = new Child('c1', 'child1', ['red'])
const c2 = new Child('c2', 'child2', ['red'])
c1.colors.push('yellow')
c1.run()
c2.run()
console.log(c1.run === c2.run)
console.log(c1.colors)
console.log(c2.colors)
问题分析
组合继承就是将原型链继承和构造函数继承方式的组合
代码分析
这里面就不一步一步的具体分析了,这两种组合方式很好的解决了上面遇到的问题(构造函数内部声明的方法重复创建的问题)
组合继承优缺点
缺点:细心的话不难发现,代码中调用了两次构造函数,做了重复操作。
Parent.apply(this, Array.from(arguments).slice(1)) // apply、call方法都是直接执行
Child.prototype = new Parent()
因此组合继承也不完美,还差点意思。
寄生组合继承
组合继承重复调用了两次构造函数,因此,可以针对这两步做一下优化
Parent.apply(this, Array.from(arguments).slice(1))
这一步是为了复制属性,因此这块代码肯定不能动
Child.prototype = new Parent()
这一步就是为了得到父类原型上面的方法,因此可以考虑让Child.prototype间接访问到Parent.prototype,从而减少一次构造函数的调用
代码实现
function Parent(name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.run = function() {
console.log(this.name)
}
function Child(id, name, colors) {
this.id = id
Parent.apply(this, Array.from(arguments).slice(1))
}
// 方法一
// function TempFunc() {}
// TempFunc.prototype = Parent.prototype
// Child.prototype = new TempFunc()
// 方法二
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
const c1 = new Child('c1', 'child1', ['red'])
const c2 = new Child('c2', 'child2', ['red'])
c1.colors.push('yellow')
c1.run()
c2.run()
console.log(c1.run === c2.run)
console.log(c1.colors)
console.log(c2.colors)
问题分析
通过代码可以看出,寄生组合继承就是:通过借用构造函数来继承属性,通过原型链来继承方法
代码分析
声明那部分的代码就不一一分析了,这里主要分析这句代码Child.prototype = Object.create(Parent.prototype)
,因为其他的代码上面已经分析过了,就不赘述了
这里需要注意的是Object.create()方法是ES5中原型式继承的规范化,当只传一个参数时,内部的行为如下
function object (o) {
function F() {};
F.prototype = o;
return new F();
}
因此和上面的代码(TempFunc)是等价的
注意 通过原型链继承方法时,不能Child.prototype = Parent.prototype这样直接赋值,至于为什么,看下面的代码
function Parent(name, colors) {
this.name = name
this.colors = colors
}
Parent.prototype.run = function() {
console.log(this.name)
}
function Child(id, name, colors) {
this.id = id
Parent.apply(this, Array.from(arguments).slice(1))
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
const c1 = new Child('c1', 'child1', ['red'])
const c2 = new Child('c2', 'child2', ['red'])
console.log(c1.run === c2.run)
console.log(Parent.prototype) // {run: ƒ, constructor: ƒ}
Child.prototype.sing = function(){}
console.log(Parent.prototype) // {run: ƒ, sing: ƒ, constructor: ƒ}
Parent类原型上本来只有一个方法run 在子类Child原型上新增加一个sing方法,这个时候打印父类原型,可以看到父类的原型上面也有了sing方法。
因为是引用类型,指向的是同一块内存地址,所以当子类在原型上面添加其它属性时,那么父类上面也会同样增加该属性,这并不是我们想要的。
Class继承
Class 可以通过extends关键字实现继承
代码实现
class Parent {}
class Child extends Parent {}
这里面就不过多的描述class的使用方式了,感兴趣的可以看看这个传送门