# 继承

参考文章:继承的八种写法·面试必问-掘金 (opens new window)

JavaScript语言没有类概念,所以JavaScript并非是基于类的继承,而是基于原型的继承
(ps:原型的概念参考这边文章 (opens new window))

JavaScript 摒弃类转而使用原型作为实现继承的基础,是因为基于原型的继承相比基于类的继承上在概念上更为简单。首先我们明确一点,类存在的目的是为了实例化对象,而 JavaScript 可以直接通过对象字面量语法轻松的创建对象。

作用

简化代码逻辑和结构,实现代码重用

# 组合继承

将原型链技术和构造函数技术结合起来,二者取其长处

// 父类构造函数School
function School() {
  this.schName = '育英'
  this.test = [1, 2, 3]
}

// 在构造函数School的原型上添加schoolName方法,打印学校名称
School.prototype.schoolName = function() {
  console.log('学校名称是:', this.schName)
}

// 子类构造函数Student
function Student(name, age) {
  // 继承父类构造函数的属性
  School.call(this)
  this.stuName = name
  this.age = age
}

// 继承父类
Student.prototype = new School()
// 将子类的构造函数手动改为Student,否则还是School
Student.prototype.constructor = Student

// 为子类添加showInfo方法,打印学生的信息
Student.prototype.showInfo = function () {
  console.log('姓名:', this.stuName, '年龄:', this.age)
}

// 测试
let boy = new Student('小明', 24)
let girl = new Student('木子', 18)
console.log(boy) // {schName: "育英", test: Array(3), stuName: "小明", age: 24}
boy.schoolName() // 学校名称是: 育英
boy.showInfo() // 姓名: 小明  年龄: 24

// 实例不会修改原型中引用类型的值
girl.test.push(4)
girl.test // [1, 2, 3, 4]
boy.test // [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 确定自身属性和原型属性

使用hasOwnProperty()区分自身属性和原型属性

接着上面的组合继承的示例

boy.hasOwnProperty('stuName') // true
boy.hasOwnProperty('showInfo') // false
boy.hasOwnProperty('schName') // true
boy.hasOwnProperty('schoolName') // false
1
2
3
4

# 确定原型和实例的关系

使用isPrototypeOf()确定原型和实例的关系, j检测一个对象是否被包含在另一个对象的原型链中

接着上面的组合继承的示例

Student.prototype.isPrototypeOf(boy) // true
School.prototype.isPrototypeOf(Student) // false
School.prototype.isPrototypeOf(boy) // true
1
2
3

# 缺点

School 会被调用 2 次:第 1 次是Student.prototype = new School();,第 2 次是调用 School.call(this)。

# 寄生组合式继承

function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype); // 创建对象,父原型的副本
  prototype.constructor = subType;                    // 增强对象
  subType.prototype = prototype;                      // 指定对象,赋给子的原型
}

// 父类构造函数School
function School() {
  this.schName = '育英'
  this.test = [1, 2, 3]
}

// 在构造函数School的原型上添加schoolName方法,打印学校名称
School.prototype.schoolName = function() {
  console.log('学校名称是:', this.schName)
}

// 子类构造函数Student
function Student(name, age) {
  // 继承父类构造函数的属性
  School.call(this)
  this.stuName = name
  this.age = age
}

// 将父类原型指向子类
this.inheritPrototype(Student, School)
// 或者可以用下面两行注释的代码替换inheritPrototype方法
// Student.prototype = Object.create(School.prototype)
// Student.prototype.constructor = Student

// 为子类添加showInfo方法,打印学生的信息
// 注意!! 子类的方法必须在上一步 也就是将父类原型指向子类后 定义
Student.prototype.showInfo = function () {
  console.log('姓名:', this.stuName, '年龄:', this.age)
}

let girl = new Student('木木', 18) // {schName: "育英", test: Array(3), stuName: "木木", age: 18}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 缺点

子类的方法必须在上一步 也就是将父类原型指向子类后 定义

# ES6中class的继承

Class 可以通过 extends 关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多

class School {
  constructor() {
    this.schName = '育英'
  }
  printSchoolName() {
    console.log('学校名称:', this.schName)
  }
}

class Student extends School {
  constructor(name, age) {
    super()
    this.stuName = name
    this.age = age
  }
  printInfo() {
    console.log('姓名:', this.stuName, '年龄:', this.age)
  }
}

let boy = new Student('小修', 8) // {schName: "育英", stuName: "小修", age: 8}
boy.printSchoolName() // 学校名称: 育英
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

super 关键字,它在这里表示父类的构造函数,用来新建父类的 this 对象。

注意

  1. 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的this 对象,而是继承父类的 this 对象,然后对其进行加工。
  2. 只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有 super 方法才能返回父类实例。

# ES5 和 ES6 实现继承的区别

  • ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面(Parent.apply(this))。
  • ES6 的继承机制完全不同,实质是先创造父类的实例对象 this (所以必须先调用 super() 方法),然后再用子类的构造函数修改 this