JS的继承方式[总结篇]

251 阅读7分钟

JavaScript的六种继承方式

JavaScript的继承方式是一个老生常谈的问题了,所以在这篇文章中我更侧重的是每种继承方式他的设计思路,同时分析其各自的优缺点,以达到强化记忆的目的。

原型链继承

  • 原理:利用对象查找属性是沿原型链这一规则,当查询对象的属性时,会先在其本身进行查找,若不存在则会通过其隐式原型(__ proto __)向上查找,直至找到原型链的尽头为止

  • 那么我们就可以通过原型链查找这一规则将父类和子类串联在一条原型链上,来实现继承的目的,即子类可以通过原型链查找到父类身上的属性和方法

  • 具体实现步骤

  1. 定义两个构造函数A和B,A中有一些属性和方法,B要继承A上的属性和方法
  2. 将A的实例对象(new A())挂载到B的原型(prototype)上,这样就实现的原型链继承
  3. 创建一个B的实例对象b,我们可以看到当b使用从A上继承过来的属性和方法时,会现在自身查找,若没有则会沿着原型链进行查找,查询路线如图

1.png

  • 代码实现
function Animal() {
  this.name = "Kim"
  this.age = 10
  this.type = ['cat', 'dog', 'tiger']
}

Animal.prototype.toString = function () {
  console.log(`${this.name} is ${this.age} years old`);
}

Animal.prototype.say = function () {
  console.log("hello");
}

function Panda() {
}

// 将Animal实例挂载在Panda的原型上
Panda.prototype = new Animal();


// Panda可以共享Animal对象的属性和方法
var panda = new Panda()
console.log(panda.name,panda.age, panda.type); // Kim 10 [ 'cat', 'dog', 'tiger' ]
panda.toString() // Kim is 10 years old
panda.say() // hello

//  修改panda的type和name属性
panda.type.push('panda')
panda.name = "盼盼"
console.log(panda.type, panda.name); // [ 'cat', 'dog', 'tiger', 'panda' ] "盼盼"

// 新建一个Panda实例对象panda2
let panda2 = new Panda()
console.log(panda2.type, panda2.name); // [ 'cat', 'dog', 'tiger', 'panda' ] "Kim"
  • 缺点
  1. 实例对象会共享引用类型的属性
  2. 子类不能向父类传递参数

借用构造函数继承

当我们创建一个实例对象时实例对象会拥有构造函数内的属性和方法

  • new在创建实例过程中做了什么?

    1. 创建一个空对象
    2. 将构造函数的this绑定到这个对象上
    3. 返回这个对象
  • 原理

    与new操作符创建实例对象的过程类似,将父类的this绑定到子类上,达到子类也能拥有父类的属性和方法的目的,可以使用call或apply将父类的this绑定到子类上,并且call和apply能传递参数给父类

  • 代码实现

// 借用构造函数继承

let Parent = function (age) {
  this.names = ['Kim', 'Jane']
  this.age = age
  this.getAge = function () {
    console.log(this.age);
  }
}

Parent.prototype.getName = function () {
  console.log(this.names);
}

Parent.prototype.height = "175cm"

let Child = function (age) {
  // 把Parent的this绑定到Child中, 第二个参数为Child传给Parent的参数
  Parent.call(this, age)
}

let child = new Child()
let child2 = new Child()

// 此时修改引用类型数据,不会影响其他实例对象(优点)
child.names.push("Lily")
console.log(child.names); // [ 'Kim', 'Jane', 'Lily' ]
console.log(child2.names); // [ 'Kim', 'Jane' ]

console.log(child.age); // undefined
console.log(child2.age); // undefined

// Child向Parent传参
let child3 = new Child(30)
console.log(child3.names); // ['Kim', 'Jane']
console.log(child3.age); // 30

// Child调用父类的方法
// 发现子类只能继承父类构造函数中的属性和方法,却无法继承父类原型上的属性和方法
child3.getAge() // 30
console.log(child3.height); // undefined
child3.getName() // child3.getName is not a function
  • 优点
    1. 因为每个实例对象都会创建一次父类的副本,所以实例对象不会共享引用类型的属性
    2. 子类可以向父类传参
  • 缺点
    1. 每创建一个实例对象都会创建一次父类的副本,代码过于臃肿

    2. 子类不能使用父类原型上的属性和方法

组合式继承

原型链继承和借用构造函数继承各有优缺点,那么我们取其精华去其糟粕,组合成一种相对完美的继承方式,这种方式就叫做组合式继承

  • 代码实现
// 组合式继承

// 父类Parent
let Parent = function (name) {
  this.name = name
  this.color = ['red', 'blue', 'black']
}

// 在其原型上写方法和属性
Parent.prototype.getName = function () {
  console.log(this.name);
}

Parent.prototype.height = "175cm"


// 子类Child
let Child = function (age, name) {
  // 将父类的this绑定到子类
  Parent.call(this, name)
  this.age = age
}

// 将父类实例对象绑定到子类的prototype上
Child.prototype = new Parent()


// 创建子类实例对象
let child = new Child(18, "小明")
let child2 = new Child(20, "小红")

child.color.push("white")
console.log(child.color); // [ 'red', 'blue', 'black', 'white' ]
console.log(child.height); // 175cm
child.getName() // 小明

console.log(child2.color); // [ 'red', 'blue', 'black' ]
console.log(child2.height); // 175cm
child2.getName() // 小红

// 我们可以看到
// 1. 实例对象不会共享父类引用类型的属性(弥补了原型链继承的缺点)
// 2. 子类可以向父类传值(弥补了原型链继承的缺点)
// 3. 子类也可以调用父类原型上的属性和方法(弥补了借用构造函数实现继承的缺点)

// 当然它也有缺点:就是每次创建的实例对象会存在两份相同的构造函数内属性(call自执行一份,原型上挂载实例一份),会造成内存的浪费

  • 优点
  1. 实例对象不会共享父类引用类型的属性(弥补了原型链继承的缺点)
  2. 子类可以向父类传值(弥补了原型链继承的缺点)
  3. 子类也可以调用父类原型上的属性和方法(弥补了借用构造函数实现继承的缺点)
  • 缺点 每次创建实例对象都会存在两份相同的属性(原型上挂载实例一次 apply或call自执行一次),代码过于臃肿

原型式继承

  • 原理 原型式继承的原理对Object.create()的实现,Object.create()的作用是让现有对象提供新对象的__proto__

  • 代码实现

function myCreateObj(obj) {
  // 定义一个空的构造函数
  function F(){}
  // 将传入的obj挂载构造函数的原型上
  F.prototype = obj
  // 最后返回出去这个构造函数的实例对象
  return new F()
}

let person = {
  name: "Lily",
  height: "165cm",
  weight: "45kg",
  color: ["pink", "black"]
}

let person1 = myCreateObj(person)
let person2 = myCreateObj(person)

console.log(person1.__proto__);
console.log(person2.__proto__);
// {
//   name: 'Lily',
//   height: '165cm',
//   weight: '45kg',
//   color: [ 'pink', 'black' ]
// }

// person1和person2继承了person上的属性

person1.color.push("red")
console.log(person1.color); // [ 'pink', 'black', 'red' ]
console.log(person2.color); // [ 'pink', 'black', 'red' ]

// 与原型链继承一样实例对象会共享引用类型属性
  • 缺点 与原型链继承一样
  1. 实例对象共享引用类型属性
  2. 子类无法向父类传参

寄生式继承

  • 原理 由于es5新增了Object.create()实现了原型式继承,寄生式继承相当于是原型式继承的再一次封装,我们可以为创建的新对象添加属性和方法,这里你可能会问难道我在原型式继承中就不能添加属性和方法吗?其实原型式继承就是对Object.create()的实现,因为原型式继承的目的就是为了直接创建并返回一个新的实例化对象。减少了内存的开销(过渡对象中无其他内容)

  • 代码实现

// 寄生式继承
function myCreateObj(obj) {
  // 创建一个新的对象,他的__proto__指向obj
  let clone = Object.create(obj)
  clone.toString = function () {
    console.log(`${this.name} is ${this.age} years old and ${this.height}`);
  }
  // 最后返回出去这个对象
  return clone
}

let person = {
  name: "Lily",
  age: 18,
  height: "165cm",
  weight: "45kg",
  color: ["pink", "black"]
}

let person1 = myCreateObj(person)
let person2 = myCreateObj(person)

person1.color.push("white")
console.log(person1.color); // [ 'pink', 'black', 'white' ]
console.log(person2.color); // [ 'pink', 'black', 'white' ]
person1.toString()
  • 缺点
  1. 实例对象共享引用类型属性
  2. 子类无法向父类传参

寄生组合式继承

  • 原理 在之前的组合式继承中有一个缺陷就是每次创建实例对象都会有两份父类构造函数的副本 那么我们能不能将两份副本缩减到一份呢?

其实问题就出现在将父类构造函数实例挂载到子类原型对象上这一步,多创建了一次父类构造函数中的属性和方法

而寄生式继承中Object.create()方法正好补齐了这一短板,他可以将现有对象作为新对象的__proto__属性,所以我们只需要定义一个过渡对象将父类的prototype属性作为该过渡对象的__proto__属性 然后再将该过渡对象赋值给子类原型对象,这样就避免了多创建一次父类构造函数中的属性和方法

  • 代码实现
let myExtends = function(parent, child) {
  // 创建一个过渡对象
  let prototype = Object.create(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype
} 

let Parent = function(name) {
  this.name = name
  this.hobby = ["reading","basketball"]
}

Parent.prototype.sayName = function() {
  console.log(this.name);
}

let Child = function(name, age) {
  Parent.call(this, name)
  this.age = age
}

// 将子类原型赋值为父类的prototype
myExtends(Parent, Child)

let child1 = new Child("小明", 12)
let child2 = new Child("小红", 13)

child1.hobby.push("computer")
console.log(child1.hobby); // [ 'reading', 'basketball', 'computer' ]
console.log(child2.hobby); // [ 'reading', 'basketball' ]
child1.sayName() // 小明
child2.sayName() // 小红

参考文章

# JavaScript深入之继承的多种方式和优缺点