> For the complete documentation index, see [llms.txt](https://1425816423.gitbook.io/my-knowledge-base/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/shen-ru-li-jie-javascript-xi-lie/javascript-zhong-de-ji-cheng.md).

# JavaScript中的继承

继承作为面向对象语言的三大特性之一，可以在不影响父类对象实现的情况下，使得子类对象具有父类对象的特性；同时还能在不影响父类对象行为的情况下扩展子类对象独有的特性，为编码带来了极大的便利。

在ES5并没有直接提供类的概念，但是我们可以通过某些方式间接实现继承，从而能利用继承的优势，增强代码复用性与扩展性。而在ES6，JavaScript提供了更接近传统语言的写法，引入了 Class（类）这个概念，作为对象的模板。通过`class`关键字，可以定义类。

## ES5的继承

在ES5并没有直接提供类的概念，但是我们可以通过某些方式间接实现继承。

### 原型链继承

将子类的prototype指向父类的实例，这样就可以让子类的实例继承父类的原型。

```javascript
// 父类
function Animal(name){
    this.name = name
}
Animal.prototype.age = 12
// 子类
function Cat (){}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
```

`Cat.prototype.constructor = Cat`这句将Cat原型上的constructor重新指向自身。

这种方法的缺点很明显，那就是无法继承父类实例的属性和方法。

### 构造继承

构造继承是通过`call`方法将父类的属性和方法绑定到子类的this中。

```javascript
// 父类
function Animal(name){
    this.name = name
}
Animal.prototype.age = 12
// 子类
function Cat (sex){
    Animal.call(this)
    this.sex = sex
}
```

这种方法的缺点很明显，就是子类无法继承父类原型上的属性和方法。

### 原型式继承

原型式继承不关注构造函数，而是只关注生成的对象和被继承的对象。

```javascript
function object(o){
    function Fn(){}
    fn.prototype = o.prototype
    return new Fn()
}
```

ES6中`Object.create` 语法可以快速实现这种继承方式。

### 寄生继承

寄生继承基于原型式继承，它在原型式继承的基础上增强对象。

```javascript
function helper(target){
  const obj = Object.create(target.prototype)
  obj.say = function(){}  // 增强
  return obj;
}
```

### 组合继承

组合继承结合了构造继承和原型链继承。通过`call`集成父类实例的方法和属性，通过`prototype`继承父类的原型

```javascript
// 父类
function Animal(name){
    this.name = name
}
Animal.prototype.age = 12
// 子类
function Cat (sex){
    Animal.call(this)
    this.sex = sex
}
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
```

这种方法既可以实现父类实例方法和属性的继承，同时也可以实现父类原型的继承。

但是缺点是父类的实例方法和属性会被继承两次，一次是`Animal.call(this)`，另一次是`Cat.prototype = new Animal()`

### 寄生组合继承

针对组合继承的缺点，可以使用寄生组合继承。

```javascript
function inheritPrototype(target,superTarget){
    let myPrototype = object.create(superTarget.prototype)
    myPrototype.constructor = target
    target.prototype = myPrototype
}

// 父类
function Animal(name){
    this.name = name
}
Animal.prototype.age = 12
// 子类
function Cat (sex){
    Animal.call(this)
    this.sex = sex
}
inheritPrototype(Cat,Animal)
```

寄生式组合继承可以算是引用类型继承的最佳模式。

## ES6的类

ES6新增class的关键字，使JavaScript正式拥有定义类的能力，不过class的本质其实使用了还是构造函数和原型，也就是说class是一个语法糖。

类和函数十分相似，但是仍要注意：

1. 类不会被提升，而函数的函数声明会被提升
2. 类是块级作用域，而函数是函数级作用域

```javascript
class Animal {
    constructor(name,age){
        this.name = name;
        this.age = age
    }
    getName{
        return this.name
    }
}
```

### 类的构造函数

`constructor`关键字用于在类定义块内部创建类的构造函数，通过`new`实例化时会自动调用`constructor`函数，并且`constructor`构造函数并不是必须的。

**使用new调用类的构造函数会执行如下操作。**

1. **在内存中创建一个新对象。**
2. **这个新对象内部的\[\[Prototype]]指针被赋值为构造函数的prototype属性。**
3. **构造函数内部的this被赋值为这个新对象（即this指向新对象）。**
4. **执行构造函数内部的代码（给新对象添加属性）。**
5. **如果构造函数返回非空对象，则返回该对象；否则，返回刚创建的新对象(即this)。**

```javascript
class Dog {
    constructor(name,age){
        this.name = name;
        this.age = age
    }
}

let dog = new Dog('a',12)
/*
{
    age: 12
    name: "a"
}
*/
```

我们尝试在类的构造函数里返回一个非空的对象

```javascript
class Cat {
    constructor(name,age){
        this.name = name;
        this.age = age
        return {a: 1}
    }
}

let cat = new Cat('a',12)
/*
{
    a: 1
}
*/
```

类的构造函数和构造函数区别就是构造函数既可以被new实例化也可以被当做普通函数调用，而类的构造函数只能被new实例化。

### 类的实例属性和方法

在类的构造函数里可以向`this`添加属性和方法，当类被`new`实例化时，`this`上的属性和方法会挂载到实例上。

```javascript
class cla {
    constructor(){
        this.age = 10
        this.getAge = function (){
    		return this.age
		}
    }
}
let a = new cla()
a
/*
{
	age: 10
	getAge: function getAge()
	[[prototype]]
}
*/
```

另外，实例属性和方法也可以不放在`constructor`里，放到类的最顶层。

```js
class cla{
a = 1
b = function(){
	return 'b'
}
}
```

### 类的静态属性和静态方法

什么是类的静态属性？类的静态属性是指该属性只归类所有，不会在实例化的时候继承。

在属性和方法前加上`static`关键字，改属性或方法就是一个静态属性或静态方法

```js
class cla{
    static a = 1
    static b = function(){
        return 'b'
    }
}
cla.a
// 1
let c = new cla()
c.a
// undefined
```

类的静态方法和静态属性只能通过类来调用它们。像我们平常使用的`Promise.all`这种方法，Promise就是一个`class`，而`all`方法就是一个静态方法。

另外要注意：静态方法里的`this`指向的是类本身。

```js
class cla{
    static a = 1
    static b = function(){
        return this.a
    }
}
cla.b()
// 1
```

因为静态方法b中`this`指向的是类本身，因此`this.a`可以看做是`cla.a`，这样可以调用静态方法和静态属性。

类静态方法的`this`指向类本身这句话很好理解，因为`this指向调用者`，对于类的静态方法来说，能调用它的只有类本身，因此它的调用者是类，那么`this`自然也就是指向类本身了。

### 类的getter和setter

类中也可以定义getter和setter

```js
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'
```

### 类的原型对象

ES6的类本质上是特殊的对象，它也有`prototype`，我们可以通过以下方式声明类原型对象上的方法和属性。

```js
class cla{
	getName(){
		return 'name'
	}

}
cla.prototype.getAge = function(){ return 18  }
cla.prototype.name = "name"
```

你会发现，类定义中显式地支持了方法，但是并没有支持数据成员。这是因为在共享目标（原型和类）上添加可变（可修改）数据成员是一种反模式。一般来说，对象实例应该独自拥有通过this引用的数据。

## 类的继承

### extends

ECMAScript 6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法，但背后依旧使用的是原型链。

ES6类支持单继承。使用extends关键字，就可以继承任何拥有\[\[Construct]]和原型的对象。很大程度上，这意味着不仅可以继承一个类，也可以继承普通的构造函数。

```js
// 父类
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}
// 派生类
class Cat extends Animal {}
```

### super

注意，派生类的构造函数中不能直接使用`this`，必须提前调用`super()`

```js
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Cat extends Animal {
    constructor(sex) {
        this.sex = sex;
    }
}

let cat = new Cat("男");
console.log(cat);
// Uncaught ReferenceError: must call super constructor before using 'this' in derived class constructor
```

正确的做法应该是

```js
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Cat extends Animal {
    constructor(sex) {
        // super里还可以传参，对应父类构造函数的参数
        super('cat',18)
        this.sex = sex;
    }
}

let cat = new Cat("男");
console.log(cat);
```

派生类的方法可以通过super关键字引用它们的原型。这个关键字只能在派生类中使用，而且仅限于类构造函数、实例方法和静态方法内部。

在类构造函数中使用super可以调用父类构造函数；在静态方法调用super可以调用父类的静态方法。

> 注意 ES6给类构造函数和静态方法添加了内部特性\[\[HomeObject]]，这个特性是一个指针，指向定义该方法的对象。这个指针是自动赋值的，而且只能在JavaScript引擎内部访问。super始终会定义为\[\[HomeObject]]的原型。

使用super要注意：

1. super只能在派生类构造函数和静态方法中使用。在派生类的构造函数调用可以为`this`添加父类的属性和方法，在静态方法里可以调用父类的静态方法。
2. 不能单独引用super关键字，要么用它调用构造函数，要么用它引用静态方法。
3. 调用super()会调用父类构造函数，并将返回的实例赋值给this。
4. super()的行为如同调用构造函数，可以给父类构造函数传参。
5. 如果没有定义类构造函数，在实例化派生类时会调用super()，而且会传入所有传给派生类的参数。
6. 在类构造函数中，不能在调用super()之前引用this。
7. 如果在派生类中显式定义了构造函数，则要么必须在其中调用super()，要么必须在其中返回一个对象。

### 抽象基类

所谓的抽象基类就是指只能被其他类继承，而不能被实例化的类。JavaScript没有直接提供抽象基类的定义，但是我们可以自己实现。

我们需要借助`new.target`，**它保存通过new关键字调用的类或函数**。我们通过在实例化时检测`new.tartget`来实现抽象基类。

`new`可以直接在类的构造函数里使用，无需自己定义。

```js
class Animal {
    constructor(name, age) {
        this.name = name;
        this.age = age;
        // 如果new.target是本身，说明自身正在被实例化
        if (new.target === Animal) {
            throw new Error("抽象基类不能被实例化");
        }
    }
}
class Cat extends Animal {
    constructor(sex) {
        // super里还可以传参，对应父类构造函数的参数
        super("cat", 18);
        this.sex = sex;
    }
}
new Animal();
// Uncaught Error: 抽象基类不能被实例化
```

### 继承内置类型

ES6类也可以继承内置类型。

```js
class SuperArray extends Array {
    shuffle() {
        // 洗牌算法
        for (let i = this.length -1; i > 0; i--) {
            const j = Math.floor(Math.random() ＊ (i + 1));
            [this[i], this[j]] = [this[j], this[i]];
        }
    }
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array);         // true
console.log(a instanceof SuperArray);   // true
console.log(a);   // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a);   // [3, 1, 4, 5, 2]
```
