JavaScript实现继承的方式

一、JavaScript实现继承的方式
1、原型链继承
2、构造函数继承
3、组合继承
4、原型式继承
5、寄生式继承
6、寄生组合式继承
二、从哪些方面分析继承方式特点、优缺点

  • 继承的属性、不会继承的属性
  • 是否实现了多继承
  • 是否能向父类构造函数传参 | 继承方式 | 关键代码 | 原理 | 重点 | 特点 | 优点 | 缺点 | 使用场景 | | —- | —- | —- | —- | —- | —- | —- | —- | | 原型链继承 | Student.prototype = new Person(); | 构造函数的实例能访问到它的原型对象上 | 让新实例的原型等于父类的实例 |
    - 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。
    - 不会继承:新实例不会继承父类实例的属性
    |
    - 简单易实现
    - 父类新增原型方法/原型属性,子类都能访问
    - 实例是子类的实例也是父类的实例
    |
    - 无法实现多继承
    - 创建子类实例时,不能向父类构造函数传参数
    - 所有新实例都会共享父类实例的属性。
    - 原型上的属性是共享的,一个实例改变了原型属性,另一个实例的原型属性也会被修改
    | | | 构造函数继承 | function Student(name, grade, site) {
    Person.call(this, name);
    } | | 用call / apply将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制)) |
    - 实例继承的属性:父类构造函数的属性
    - 不会继承:新实例不会继承父类原型的属性
    |
    - 创建子类实例时,可以向父类传递参数
    - 可以实现多继承(call多个对象)
    - 不需要修复构造函数指向
    |
    - 方法在构造函数中定义,无法复用
    - 每次用都要重新调用
    - 只能继承父类的实例属性,不能继承原型属性、方法
    - 在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式
    - 实例并不是父类的实例,而只是子类的实例
    - 每个新实例都有父类构造函数的副本,臃肿
    | | | 组合继承 | function Student(name, grade, site) {
    Person.call(this, name);
    }

    Student.prototype = new Person() // 继承方法
    Student.prototype.constructor = Student | 将原型链与借用构造函数的技术组合起来。在原型中定义公用的属性方法,在实例中定义私有的属性方法。实现组合式继承。 | 结合了以上两种模式的优点:传参、复用 | |
    - 可以继承实例属性、方法,也可以继承原型属性、方法
    - 可传参、可复用
    - 实例既是子类的实例,也是父类的实例
    |
    - 调用了两次父类构造函数,耗内存
    - 一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
    - 需要修复构造函数指向
    | 最常用 | | 原型式继承 | function object(o) {
    function F() {}
    F.prototype = obj;
    return new F();;
    } | | 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。
    Object.create()就是这个原理
    类似于复制一个对象,用函数来包装 |
    - 所有实例都会继承原型上的属性
    | |
    - 不支持多继承
    - 实例是父类的实例
    - 包含引用类型值的属性始终都会共享相应的值
    - 无法实现复用
    | 只是想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的 | | 寄生式继承 | function object(o) {
    function F() {}
    F.prototype = obj;
    return new F();
    }

    // 给原型式继承套个壳子传递参数
    function Student(obj) {
    const person = object(obj);
    person.grade = ‘g’;
    return person;
    }

    const person = new Person()
    const stu = Student(person) | | 给原型式继承外面套了个壳子 | |
    - 没有创建自定义类型
    - 因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象
    |
    - 不支持多继承
    - 实例是父类的实例,不是子类的实例
    - 因为只是在父类的实例上添加属性、方法而已
    - 做不到函数复用
    - 因为没用到原型
    | 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。 | | 继承组合式继承 | function Student(name, grade, site) {
    Person.call(this, name);
    this.grade = grade;
    }

    let proto = Object.create(Person.prototype); / / 创建超类型原型的一个副本
    proto.constructor = Student; // 修复构造函数指向
    Student.prototype = proto; // 继承方法

    const stu = new Student(‘asuncat’, 3, ‘https://yquanmei.github.io‘); | | | | 通过Object.create()来代替给子类原型赋值的过程,解决了两次调用父类构造函数的问 |
    - 修复了组合继承的问题
    | |

| ☆-1【示例】父类```javascript function Person(name) { this.name = name; this.say = function() { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function() { return this.property }

  1. |
  2. | --- |
  3. > 超类型:父级、被继承的对象
  4. <a name="TScwC"></a>
  5. ## 原型链继承
  6. 一、将父类的实例作为子类的原型
  7. - 实现的本质:重写子类型的原型对象,代之以一个新类型的实例
  8. - 子类型的新原型对象中有一个内部属性Prototype指向了「父类」的原型,还有一个从「父类」原型中继承过来的属性constructor指向了「父类」构造函数
  9. | ☆-1【示例】```javascript
  10. // 实现原型链的一种基本模式
  11. function Person() {
  12. this.name = name;
  13. this.say = function() {
  14. console.log('1 + 1 = 2');
  15. }
  16. }
  17. Person.prototype.listen = function() {
  18. console.log('hello world');
  19. }
  20. function Student() {
  21. }
  22. // 继承,用父类型的一个实例来重写子类型的原型对象
  23. Student.prototype = new Person(); // 关键
  24. const stu = new Student();
  25. stu.grade = 3;
  26. console.log(stu.grade); // 3
  27. stu.say(); // 1 + 1 = 2
  28. stu.listen() // hello world

1、其中,Student 继承了Person, 而继承是通过创建Person的实例,将该实例赋值给Student的原型实现的。
2、最终的原型链:stu指向Student的原型,Student的原型又指向Person的原型,Person的原型又指向Object的原型(所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype) | | —- |

二、实例是子类的实例也是父类的实例

| ☆-1【示例】```javascript stu instanceof Student // true stu instanceof Person // true

 |
| --- |

**存在的问题**

- prototype里有个属性ocnstructor指向构造函数本身,但是,「子类」的原型已经被父类的实例取代了,所以指向也不正确,所以需要修复构造函数指向
| ☆-1【示例】```javascript
function Student() {}

console.log(Student.prototype.constructor);

Student.prototype = new Person(); // 关键

const stu = new Student()
stu.grade = 3
console.log(stu.grad) //3
stu.say() // 1 + 1 = 2
stu.listen() // hello world

console.log(Student.prototype.constructor)

image.png | | —- |

解决问题

| ☆-1【示例】```javascript Student.prototype.constructor = Student

![image.png](https://cdn.nlark.com/yuque/0/2022/png/355497/1652755350921-e2fda151-5d21-46d0-a9ed-ec1673e7f112.png#clientId=u154af8e7-9b9b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=388&id=u72f4de00&margin=%5Bobject%20Object%5D&name=image.png&originHeight=388&originWidth=1002&originalType=binary&ratio=1&rotation=0&showTitle=false&size=126294&status=done&style=none&taskId=u4199ea7b-eaa7-4599-a66c-5ca2e7ced11&title=&width=1002) |
| --- |


<a name="PKoao"></a>
## 构造函数继承/伪造对象/经典继承
一、在一个类中执行另一个类的构造函数,通过apply/ call函数设置this的指向,这样就可以得到另一个类的所有属性

| ☆-1【示例】```javascript
function Person() {
  this.name = name;
  this.say = function () {
    console.log('1 + 1 = 2');
  }
}
Person.prototype.listen = function () {
  console.log('hello world');
}
function WebsiteMaster(site) {
  this.site = site
}

function Student(name, grade, site) {
  // 关键代码
  Person.call(this, name); // 继承Person,在这里还可以给超类型构造函数传参
  WebsiteMaster.call(this, site)
  this.grade = grade
}

const stu = new Student('asuncat', 3, 'https://yquanmei.github.io');
console.log(stu.name, stu.grade, stu.site); // asuncat, 3, 'https://yquanmei.github.io'

stu.say(); // 1 + 1 = 2
stu.listen() // Uncaught TypeError: stu.listen is not a function

| | —- |

二、实例并不是父类的实例,而只是子类的实例

| 【示例】```javascript stu instanceof Student // true stu instanceof Person // false

 |
| --- |

<a name="O7net"></a>
## 组合继承/伪经典继承
一、原型链继承  + 构造函数继承

| ☆-1【示例】```javascript
function Person(name) {
  this.name = name;
  this.say = function() {
    console.log('1 + 1 = 2');
  }
}
Person.prototype.listen = function() {
  return this.property
}

function WebsiteMaster(site) {
  this.site = site;
}

function Student(name, grade, site) {
  Person.call(this, name); // 继承属性
  WebsiteMaster.call(this, site);
  this.grade = grade
}

Student.prototype = new Person() // 继承方法 // 将Student的prototype对象指向一个Animal实例,相当于完全删除了prototype对象原先的值,然后赋予了一个新值

// 任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有Student.prototype = new Person()这一行,Student.prototype.constructor指向Student,加了这一行后,Student.prototype.constructor指向Animal 
// 每个实例也有一个constructor属性,默认调用prototype的constructor属性。因此,在执行Student.prototype = new Person()后,stu的consturctor也指向Person, 这会造成继承链的紊乱(stu命名是调用构造函数Student生成的)
// 因此,我们必须将Student.prototype的constructor改成Student
Student.prototype.constructor = Student 

const stu = new Student('asuncat', 3, 'https://yquanmei.github.io')

console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io

stu.say(); // 1 + 1 = 2
stu.listen(); // hello world

console.log(stu.constructor);

image.png | | —- |

二、组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。而且,使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。
三、如果替换了prototype对象,下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

| 【示例】```javascript o.prototype = {}; o.prototype.constructor = o;

 |
| --- |

<a name="XpAlu"></a>
## 原型式继承
一、为父类实例添加属性、方法,作为子类实例。
<a name="pXTrc"></a>
### es5:自定义一个函数来实现原型式继承
一、道格拉斯·克罗克福德在一篇文章中介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。它的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义模型。为了达到这个目的,它给出了如下函数
```javascript
// 封装一个函数容易,用来输出对象和承载继承的原型
function object(o) {
  funciton F() {} // 创建临时性构造函数
  F.prototype = o; // 将传入的对象作为这个构造函数的原型
  return new F(); // 返回临时类型的一个新实例
}

1、在 object() 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。实质上,object() 对传入其中的对象执行了一次浅复制

| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function () { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function () { return this.property }

function object(o) { function F() { } F.prototype = o; return new F(); }

const person = new Person(‘asuncat’);

const stu = object(person);

stu.grade = 3; stu.study = function () { console.log(‘FrontEnd’); }

console.log(stu.name, stu.grade); // asuncat, 3

stu.say(); // 1 + 1 = 2 stu.listen(); // hello world

stu.study() // FrontEnd

 |
| --- |

<a name="xERnT"></a>
### es6:使用 Object.create() 方法实现原型式继承
一、Object.create()方法接收两个参数:一是用作新对象原型的对象和一个为新对象定义额外属性的对象。在传入一个参数的情况下,此方法与 object() 方法作用一致。 在传入第二个参数的情况下,指定的任何属性都会覆盖原型对象上的同名属性。

| ☆-2【示例】```javascript
var person = {
  name: 'asuncat',
  colors: ['red', 'green', 'blue']
}

var anotherPerson1 = Object.create(person, {
  name: {
    value: 'tom'
  }
})

var anotherPerson2 = Object.create(person, {
  name: {
    value: 'jerry'
  }
})

anotherPerson1.colors.push('purple');

console.log(anotherPerson1.name); // tom
console.log(anotherPerson2.name); // jerry
console.log(anotherPerson1.colors); // ['red,green,blue,purple']
console.log(anotherPerson2.colors); // ['red,green,blue,purple']

| | —- |

寄生式继承

一、为父类实例添加属性、方法,作为子类实例。(原理和原型式继承一样)

| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function() { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function() { return this.property }

function object(o) { function F() {} F.prototype = o; return new F(); }

function Student(name, grade) { const person = object(new Person(name));

person.grade = grade person.study = function() { console.log(‘FrontEnd’); }

return person; }

const stu = new Student(‘asuncat’, 3)

console.log(stu.name, stu.grade); // asuncat, 3 stu.say(); // 1 + 1 = 2 stu.listen(); // hello world

stu.study(); // FrontEnd

 |
| --- |

| ☆-2【示例】```javascript
// 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象
function createPerson(o){
  var clone = Object.create(o); // 通过 Object.create() 函数创建一个新对象
  clone.study = function(){ // 增强这个对象
    alert("FrontEnd");
  };
  return clone; // 返回这个对象 
}

| | —- |

寄生组合式继承

一、通过Object.create()来代替给子类原型赋值的过程,解决了两次调用父类构造函数的问题

| ☆-1【示例】```javascript function Person(name) { this.name = name; this.say = function () { console.log(‘1 + 1 = 2’); } } Person.prototype.listen = function () { return this.property }

function WebsiteMaster(site) { this.site = site }

function Student(name, grade, site) { Person.call(this, name); // 继承属性 WebsiteMaster.call(this, site); this.grade = grade; }

let proto = Object.create(Person.prototype); // 核心 // 创建超类型原型的一个副本 proto.constructor = Student; // 核心 // 修复构造函数指向 Student.prototype = proto // 核心 // 继承方法

const stu = new Student(‘asuncat’, 3, ‘https://yquanmei.github.io‘);

console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io

stu.say(); // 1 + 1 = 2

stu.listen(); // hello world

```javascript
function Person(name) {
  this.name = name;
  this.say = function () {
    console.log('1 + 1 = 2');
  }
}
Person.prototype.listen = function () {
  return this.property
}

function WebsiteMaster(site) {
  this.site = site
} 

function Student(name, grade, site) {
  Person.call(this, name); // 继承属性
  WebsiteMaster.call(this, site);
  this.grade = grade;
}

let proto = object(Person.prototype); // 核心 // 创建超类型原型的一个副本
proto.constructor = Student; // 核心 // 继承方法
Student.prototype = proto; // 核心 // 修复构造函数指向

const stu = new Student('asuncat', 3, 'https://yquanmei.github.io');

console.log(stu.name, stu.grade, stu.site); // asuncat, 3, https://yquanmei.github.io

stu.say(); // 1 + 1 = 2

stu.listen(); // hello world

| | —- |

| ☆-2【示例】```javascript // 通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型 function SuperType(name){ this.name = name; this.colors = [“red”,”green”,”blue”]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age){ SuperType.call(this,name); this.age = age; } // 创建超类型原型的一个副本 var anotherPrototype = Object.create(SuperType.prototype); // 重设因重写原型而失去的默认的 constructor 属性 anotherPrototype.constructor = SubType; // 将新创建的对象赋值给子类型的原型 SubType.prototype = anotherPrototype;

SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType(“luochen”,22); instance1.colors.push(“purple”); alert(instance1.colors); // “red,green,blue,purple” instance1.sayName(); instance1.sayAge();

var instance2 = new SubType(“tom”,34); alert(instance2.colors); // “red,green,blue” instance2.sayName(); instance2.sayAge();

1、这个例子的高效率体现在它只调用一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要,多余的属性。与此同时,原型链还能保持不变;因此还能正常使用instanceof操作符和isPrototype()方法 |
| --- |

<a name="WfInL"></a>
### 为什么不可以直接把父类原型赋值给子类原型来实现呢
一、因为直接赋值的话,那就是引用关系

| ☆-2【示例】```javascript
function Person(name) {
  this.name = name;
  this.say = function () {
    console.log('1 + 1 = 2');
  }
}
Person.prototype.listen = function () {
  return this.property
}
function WebsiteMaster(site) {
  this.site = site
}

function Student(name, grade, site) {
  Person.call(this, name)            // 继承属性
  WebsiteMaster.call(this, site)
  this.grade = grade
}

Student.prototype = Person.prototype        // 继承方法
Student.prototype.constructor = Student   // 修复实例

const stu = new Student('clz', 3, 'https://clz.vercel.app')

console.log(stu.name, stu.grade, stu.site)  // clz, 3, https://clz.vercel.app

stu.say()   // 1 + 1 = 2
stu.listen()    // エウテルペ


Student.prototype.listen = function () {
  console.log('EGOIST')
}

console.log(Person.prototype.listen)

image.png
1、修改Student原型上的方法时,Person上的原型上也会跟着变化。
2、Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。所以,此时修改Student原型上的方法时,Person的原型上的不会跟着变化。 | | —- |

| ☆-2【示例】es6之前没有Object.create()方法,可以自己实现(实际就是原型式继承的关键函数)
1、关键:
- 接收一个对象obj
- 返回一个新对象newObj
- 让newObj.**proto** === obj
javascript function object(obj) { function F() {} // 新的构造函数 F.prototype = obj; // 继承传入的参数obj return new F(); // 返回新的函数对象 } | | —- |