JavaScript在ES5中实现继承
Tags: javascript
Published: 2022-04-10 15:35:35


# JS在ES5中实现继承的几种方式

## 前言

> 首先你要知道，在JavaScript中是没有类这个概念的，ES6后我们可以通过`extend`和`super`关键字实现继承，它们只是ES5继承的语法糖，让开发者们实现继承不再需要那么繁琐，最终还是需要通过`babel`转为ES5的代码。

我列出了六种实现继承的方式，但前四种方式各有弊端，了解一下就好。

演示代码我会以`Person`构造函数作为父类，`Student`作为子类。

## 一、混入式继承

使用`for in`循环对象，将父类复制到子类，实现继承：

```javascript
let obj1 = {
  name: "bob",
  hobbies: ["coding","guitar"],
}

let obj2 = {}

// 循环父类
for (key in obj1) {
  obj2[key] = obj1[key]
}

console.log(obj2) // { name: 'bob', hobbies: [ 'coding', 'guitar' ] }
```

这种方式可以简单实现继承，基本数据类型是值拷贝，而引用数据类型是地址拷贝，一改全改，造成数据不安全的问题

不推荐使用这种方式！

## 二、原型式继承

原型式继承的主要思路就是让**子类的原型指向父类的原型**，实现继承。

弊端：数据安全问题，子类会修改父类的原型对象，造成原型链结构混乱。

```javascript
// 父类
function Person(name,age){
  this.name = name
  this.age = age
}

// 父类原型添加方法
Person.prototype.say = function() {
  console.log(this.name, this.age, this.height)
}

// 子类
function Student(name, age, height) {
  this.name = name
  this.age = age
  this.height = height
}

// 子类的原型指向父类的原型
Student.prototype = Person.prototype
// 指回原型的类型
Student.prototype.constructor = Student

// 实例化对象
let std = new Student("bob", 18, 1.88)

std.say() // bob 18 1.88
```

已经实现了继承，能够使用父类原型的方法，但有时候我们需要给子类原型单独添加方法，父类的原型也会随之添加，这显然是不合理的，所以不推荐使用这种方式！

## 三、原型链继承

思路：**子类的原型对象指向父类的洗礼对象**。

弊端：**只能继承原型成员**，不能继承实例成员

```javascript
function Person(name, age){
  this.name = name
  this.age = age
}

Person.prototype.say = function() {
  console.log(this.name, this.age, this.height)
}

function Student(name, age, height) {
  this.name = name
  this.age = age
  this.height = height
}

// 子类原型对象指向父类的实例对象
Student.prototype = new Person()
Student.prototype.constructor = Student

let std = new Student("bob", 18, 1.88)

std.say() // bob 18 1.88
```

这种方式对原型式继承做了优化，使子类和父类不再共用一个原型对象，但子类依然可以继承父类的方法，这样结构会很清晰，子类也可以为自己添加独有的方法。

## 四、借用构造函数继承

和上一种方式相反，这种方式只能继承实例成员，不能继承原型成员，我们需要把它们组合起来，我会在下一个标题实现组合继承。

思路：

1. 让父类构造函数在子类内部运行
2. 将父类构造函数赋值给子类实例的一个属性 (this.func = Person)
3. 子类实例调用这个方法，父类内部的this指向子类的实例

注意：不要直接在子类构造函数内部直接调用父类构造函数，此时`this`指向的是window

```javascript
function Person(name, age){
  this.name = name
  this.age = age
}

function Student(name, age, height) {
  // Person() xx
  
  this.func = Person;
  // 参数传给父类构造函数
  this.func(name, age) // 构造函数内部的this指向当前实例化对象
  this.height = height
}

let std = new Student("bob", 18, height)

console.log(std.name, std.age, std.height) // bob 18 1.88
```

使用`call`或`apply`优化代码：

```javascript
function Person(name, age){
  this.name = name
  this.age = age
}

function Student(name, age, height) {
  // Person() xx
 
  Person.call(this, name, age)
  // Person.apply(this, [name, age])
  this.height = height
}

let std = new Student("bob", 18, 1.88)

console.log(std.name, std.age, std.height) // bob 18 1.88
```

这种方式可以让我们不再需要在子类构造函数的内部写那么多重复的代码，比如`this.name = name, this.age = age`....

## 五、组合继承

该方式结合了原型链继承和借用构造函数继承，既可以继承原型成员，又可以继承实例成员，也是我推荐大家使用的一种方式。

```javascript
function Person(name, age){
  this.name = name
  this.age = age
}

function Student(name, age, height) {
  Person.call(this, name, age)
  this.height = height
}

Person.prototype.say = function() {
  console.log("Info: " + this.name, this.age, this.height)
}

Student.prototype = new Person()
Student.prototype.constructor = Student


let std = new Student("bob", 18, 1.88)

std.say() // Info: bob 18 1.88

console.log(std.name, std.age, std.height) // bob 18 1.88
```