1.原型链继承
function Fun1() {
this.name = 'parent'
this.play = [1, 2, 3]
}
function Fun2() {
this.type = 'child'
}
Fun2.prototype = new Fun1()
let s1 = new Fun2()
let s2 = new Fun2()
s1.play.push(4)
console.log(s1.play, s2.play) // [1, 2, 3, 4] [1, 2, 3, 4]
可以看到两个子类实例的属性都被影响了,这是因为通过原型链继承的子类实例的属性是共享的
原型链继承的弊端
-
共享引用类型属性
-
函数无法传递参数
-
原型链过长导致性能问题
-
难以维护和调试
2.构造函数继承
function Fun1() {
this.name = 'parent'
}
Fun1.prototype.getName = function () {
return this.name
}
function Fun2() {
Fun1.call(this)
this.type = 'child'
}
let child1 = new Fun2()
let child2 = new Fun2()
child2.type = 'xxxx'
console.log(child1, child2) // Fun2 { type: 'child', name: 'parent' } Fun2 { type: 'xxxx', name: 'parent' }
console.log(child1.getName()) // child1.getName is not a function
可以看到构造函数继承确实不会共享属性了,但是构造函数继承只继承了父类的实例属性和方法,而不会继承父类原型上的方法
构造函数继承的弊端
-
无法复用父类原型上的方法
-
每个子类实例都会创建新的父类实例(每次创建子类实例时,都会调用一次父类构造函数,这会导致不必要的资源消耗,尤其是在父类构造函数中有复杂初始化逻辑或大量计算时。)
-
无法传递参数给父类构造函数(如果父类构造函数需要参数,子类构造函数必须显式地传递这些参数,否则父类构造函数中的初始化逻辑可能无法正常工作。)
-
难以维护和调试(构造函数继承的代码结构相对复杂,特别是在多层继承的情况下,可能会导致代码难以理解和维护。此外,调试时也更难追踪继承链中的问题。)
3.组合继承(伪经典继承)
function Fun1(name) {
this.name = name
this.play = [1, 2, 3]
}
Fun1.prototype.getName = function () {
return this.name
}
function Fun2() {
Fun1.call(this, 'haha')
this.type = 'child'
}
Fun2.prototype = new Fun1()
// 修复构造函数指针,确保子类的 constructor 属性正确指向其自身的构造函数
// 如果不写这段代码,下面的 child1 和 child2 的 constructor 就会指向 Fun1
Fun2.prototype.constructor = Fun2
let child1 = new Fun2()
let child2 = new Fun2()
console.log(child1.constructor === Fun2) // true
console.log(child2.constructor === Fun1) // false
child1.play.push(4)
console.log(child1.play, child2.play) // [1, 2, 3, 4] [1, 2, 3]
console.log(child1.getName(), child2.getName()) // haha haha
组合继承的优点
- 避免了原型链继承的共享属性问题(每个子类实例都有自己的属性副本,不会共享父类实例中的引用类型属性。)
- 可以继承父类原型上的方法(子类不仅继承了父类的实例属性,还可以继承父类原型上的方法,避免了重复定义方法的问题。)
- 灵活性高(可以在子类构造函数中调用父类构造函数时传递参数,确保父类初始化逻辑正常工作。)
组合继承的缺点
- 调用两次父类构造函数(组合继承的主要问题是它会调用两次父类构造函数:一次是在创建子类原型时 (
new Fun1()
),另一次是在子类构造函数中 (Fun1.call(this, 'haha')
)。这会导致不必要的性能开销,尤其是在父类构造函数中有复杂初始化逻辑或大量计算时。) - 原型对象的冗余属性(由于
Fun2.prototype = new Fun1()
,子类的原型对象会包含父类实例的所有属性,这可能会导致不必要的内存占用。)
4.原型式继承
let parent = {
name: 'parent',
friends: [1, 2, 3],
getName: function () {
return this.name
},
}
let child = Object.create(parent)
child.name = 'child'
child.friends.push(666)
console.log(parent.getName(), child.getName()) // parent child
console.log(parent.friends, child.friends) // [1, 2, 3, 666] [1, 2, 3, 666]
原型式继承的弊端
-
共享引用类型属性(如果父对象中的属性是引用类型(如数组、对象),所有子对象会共享这些属性。修改一个子对象的引用类型属性会影响到其他子对象。)
- 无法传递参数(原型式继承不能像构造函数继承那样在创建子对象时传递参数给父对象的初始化逻辑。)
-
缺乏构造函数支持(原型式继承没有构造函数的概念,因此无法像类或构造函数那样进行复杂的初始化操作。)
-
难以扩展和维护(使用原型式继承的代码结构相对简单,但在大型项目中可能会导致代码难以扩展和维护。特别是当需要添加更多复杂的功能或逻辑时,原型式继承的方式显得不够灵活。)
-
性能问题(由于所有子对象共享父对象的属性和方法,查找属性或方法时需要遍历原型链,这可能会影响性能,尤其是在多层继承的情况下。)
5.寄生式继承
let parent = {
name: 'parent',
friends: [1, 2, 3],
getName: function () {
return this.name
},
}
function clone(params) {
let copyChild = Object.create(params)
copyChild.getFriends = function () {
return this.friends
}
return copyChild
}
let child = clone(parent)
child.name = 'child'
child.friends.push(999)
console.log(parent.friends, child.friends) // [1, 2, 3, 999] [1, 2, 3, 999]
console.log(child.getFriends())
寄生式继承的弊端
-
代码复杂度增加(寄生式继承增加了代码的复杂性,因为它引入了一个额外的函数来封装继承逻辑。这使得代码更难理解和维护,尤其是在大型项目中。)
-
共享引用类型属性(尽管寄生式继承可以增强对象,但它仍然无法解决原型式继承中的共享引用类型属性问题。所有子对象会共享父对象中的引用类型属性,修改一个子对象的引用类型属性会影响到其他子对象。)
-
缺乏构造函数支持(寄生式继承没有构造函数的概念,因此无法像类或构造函数那样进行复杂的初始化操作。每次创建新对象时都需要调用封装函数,并且需要手动传递参数。)
-
性能问题(由于每次创建新对象时都需要调用封装函数并执行额外的逻辑,这可能会导致性能开销,尤其是在频繁创建对象的情况下。)
-
难以扩展和维护(寄生式继承的代码结构相对复杂,特别是在多层继承或需要添加更多功能时,代码会变得难以扩展和维护。此外,调试和理解这种继承模式也更加困难。)
6.寄生组合式继承
function Parent() {
this.name = 'parent'
this.play = [1, 2, 3]
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this) // 借用构造函数继承属性
this.friends = 'child'
}
function clone(parent, child) {
child.prototype = Object.create(parent.prototype) // 设置子类原型为父类原型的副本
child.prototype.constructor = child // 修复构造函数指针
}
clone(Parent, Child)
Child.prototype.getFriends = function () {
return this.friends
}
let person = new Child()
console.log(person.getName(), person.getFriends()) // parent child
寄生组合式继承:是一种高效的继承模式,结合了构造函数继承和原型链继承的优点,避免了它们各自的缺点。它确保了每个子类实例都有自己的属性副本,同时可以继承父类原型上的方法,并且不会调用两次父类构造函数。这种继承方式在现代 JavaScript 开发中非常常见,特别是在需要复杂继承结构的情况下。
7.通过Es6中 extends 关键字继承
class Parent {
constructor() {
this.name = 'Parent'
this.play = [1, 2, 3]
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor() {
// 在子类构造函数中调用父类构造函数,确保继承父类的实例属性,允许子类覆盖或扩展父类的功能。
super()
this.friends = 'child'
}
getFriends() {
return this.friends
}
}
相当于是 寄生组合式继承 的语法糖,使用起来更简洁高效。