继承的多种方式和优缺点

323 阅读4分钟

原型链继承

function Parent() {
  this.users = ['userA', 'userB', 'userC']
}
// 关键点1:person1和person2可以继承构造函数`prototype`上的属性和方法;
Parent.prototype.getUsers = function() {
  console.log(this.users)
}
Parent.prototype.sex = '男'
Parent.prototype.list = []

function Child() {
  this.age = 18
}
Child.prototype = new Parent()
var person1 = new Child()
// 关键点2:构造函数中以及原型对象上【引用类型】的属性都会被共享(list和users)
person1.users.push('userD')
person1.list.push('userF')
// 关键点3:这里person1实例本身并没有sex属性,这里实际上是给person1添加了sex属性
person1.sex = '女'
var person2 = new Child()
console.log(person1.sex, person2.sex)
console.log(person1, person2)

step1.png

如图所示,可以很明显的看出原型链继承的优缺点

  • 优点: 可以同时继承构造函数中和原型对象prototype上的属性和方法;
  • 缺点: 引用类型的属性被所有实例共享(实际上是所有的属性,只是基本类型的属性无法被实例修改);

构造函数继承(经典继承)

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
// 关键点:prototype属性无法被继承
Parent.prototype.sex = '男'

function Child(user) {
  // 关键点:可以向父类传参
  Parent.call(this, user)
  this.age = 18
}
var person1 = new Child('person1')
// 关键点:users属性不会被共享
person1.users.push('userD')               
var person2 = new Child('person2')
console.log(person1.age, person2.age)
console.log(person1.sex, person2.sex)
console.log(person1.users, person2.users)

step2.png

通过这个例子,可以看到,构造函数继承对比于原型链继承方式:

  • 优点:
    1. 避免了引用类型的属性被所有实例共享;
    2. 可以向父类传参
  • 缺点:只能继承构造函数中的属性和方法,无法继续原型对象prototype上的属性和方法;

组合继承

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  user && this.users.push(user)
}
Parent.prototype.sex = '男'

function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
// Parent.call(this, user)和new Parent()重复继承了Parent构造函数中的属性
Child.prototype = new Parent()
var person1 = new Child('person1')
person1.users.push('userD')               
var person2 = new Child('person2')
console.log(person1, person2)

step3.png

组合继承

  • 优点:融合了原型链继承和构造函数的优点;
  • 缺点:
    1. 创建了多余的属性(如图所示的users)
    2. Parent方法被调用了两次(new Parent(),Parent.call(this, user))

组合继承优化1

  • 针对上面的问题1,我们先复习下原型链的知识点
// 实例对象的原型`__proto__`指向构造函数的原型对象
new Parent().__proto__ === Parent.prototype // true
// 构造函数原型对象的`constructor`属性指向构造函数本身
Child.prototype.constructor === Child // true

那么我们可以做出优化代码如下:

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
// 由于Parent.call(this, user),已经继承了Parent构造函数中的属性,只需要再继承Parent原型对象上的属性即可
Child.prototype = Parent.prototype
// 期望:Child.prototype.constructor === Child;实际:Child.prototype.constructor === Parent
console.log(Child.prototype.constructor === Child) // false
console.log(Child.prototype.constructor === Parent) // true
var person1 = new Child('person1')
person1.users.push('userD')               
var person2 = new Child('person2')
console.log(person1, person2)

step4.png

对比优化前后端结果,不难看出,创建的多余属性(users)已经没有了,且Parent构造函数只被调用了一次;结合之前的原型链的知识,我们期望:Child.prototype.constructor === Child,然而实际上Child.prototype.constructor === Parent

接下来让我们继续优化下代码

组合继承优化2

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
// 疑问:是否有问题,怎么优化
Child.prototype = Parent.prototype
// 优化代码
Child.prototype.constructor = Child
var person1 = new Child('person1')             
var person2 = new Child('person2')
console.log(Child.prototype.constructor === Child) // true
console.log(person1.constructor === Child) // true

代码到这里基本上就已经完成了,但是这里还有个问题,我们知道引用类似的对象直接赋值时是浅拷贝,所以这里修改Child的prototype属性值会覆盖Parent.prototype的同名属性,增加新属性

如下示例,我们希望Child继承Parent的属性,而不会直接去修改Parent本身的属性,不会对Parent的实例造成影响

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
// 期望:男,undefined; 实际女,70kg
console.log(parent.sex, parent.weight) // 女,70kg

组合继承优化3

上面提到,由于Parent.call(this, user),已经继承了Parent构造函数中的属性,只需要再继承Parent原型对象上的属性即可,结合之前原型链的知识可以优化代码如下

Parent.prototype = new Parent().__proto__
function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
console.log(parent.sex, parent.weight) // 男,undefined

step5.png

补充,关于Object.create(obj)

Object.create(obj)创建的其实是obj对象的原型引用

// 模拟Object.create(obj)
function createObj(obj) {
  var prototype = obj
  function Fn() {}
  Fn.prototype = prototype
  // new Fn().__proto__ == Fn.prototype
  return new Fn()
}

修改之前的代码,如下所示,我们得到了完全一样的结果

function Parent(user) {
  this.users = ['userA', 'userB', 'userC']
  this.users.push(user)
}
Parent.prototype.sex = '男'
function Child(user) {
  Parent.call(this, user)
  this.age = 18
}
function createObj(obj) {
  var prototype = obj
  function Fn() {}
  Fn.prototype = prototype
  return new Fn()
}
Child.prototype = createObj(Parent.prototype)
Child.prototype.constructor = Child
// 会覆盖parent中的同名属性
Child.prototype.sex = '女'
Child.prototype.weight = '70kg'
var person = new Child('person')
var parent = new Parent('Parent')
console.log(person.sex, person.weight) // 女,70kg
console.log(parent.sex, parent.weight) // 男,undefined

总结

  • 本文讲述了继承的多种方式和优缺点
  • 如何去一步步的去实现函数的继承,以及如何去优化
  • 本文涉及到原型链、基本数据类型、引用数据类型、Object.create()等相关知识点