# 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]
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://1425816423.gitbook.io/my-knowledge-base/qian-duan-ji-shu/shen-ru-li-jie-javascript-xi-lie/javascript-zhong-de-ji-cheng.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
