阅读本章节内容时,请先了解原型链。

概念

让 B 构造函数的实例能够使用 A 构造函数的属性和方法,此时有:

  • B构造函数 是 A构造函数 的子类
  • A构造函数 是 B构造函数 的父类

父类与子类

  1. // 父类
  2. function Person(name, age) {
  3. this.name = name;
  4. this.age = age;
  5. }
  6. Person.prototype.sayHi = function () { console.log('Hello World') }
  7. const PersonInstance = new Person('syukinmei', 18);
  8. console.log(PersonInstance);
  9. // 子类
  10. function Student(gender) {
  11. this.gender = gender;
  12. }
  13. Student.prototype.study = function () { console.log('学习中。。。') }
  14. const StudentInstance = new Student('男');
  15. console.log(StudentInstance);

image.png

原型链继承

利用改变 原型链 的方式来达到继承
直接把父类的实例当作子类的 prototype

核心代码

  1. // 1
  2. Student.prototype = PersonInstance;
  3. // 2
  4. Student.prototype = new Person('ebiebi', 16);
  1. // 子类
  2. function Student(gender) {
  3. this.gender = gender;
  4. }
  5. // Student.prototype = PersonInstance;
  6. Student.prototype = new Person('ebiebi', 16); // 核心代码
  7. Student.prototype.study = function () { console.log('学习中。。。') }
  8. const StudentInstance = new Student('男');
  9. console.log(StudentInstance);
  10. StudentInstance.sayHi(); // Hello World
  11. // instanceof 判断元素是否在另一个元素的原型链上
  12. // StudentInstance 继承了Person的属性,返回true
  13. console.log(StudentInstance instanceof Person); // true

image.png

分析

重点:让新实例的原型等于分类的实例
优点

  1. 新实例可以继承的属性有:实例的构造函数的属性、父类构造函数的属性、父类原型的属性。(新实例不会继承父类实例的属性)

缺点

  1. 子类型在实例化时不能给父类型的构造函数传参,一个构造函数的内容,在两个位置传参。
  2. 继承单一,继承来的属性不在子类实例的身上。
  3. 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改)

下面的例子揭示了问题3

  1. function SuperType() {
  2. this.colors = ['red', 'blue', 'green'];
  3. }
  4. function SubType() { }
  5. // 继承 SuperType
  6. SubType.prototype = new SuperType();
  7. let instance1 = new SubType();
  8. instance1.colors.push('yellow');
  9. console.log(instance1.colors); // ['red', 'blue', 'green', 'yellow']
  10. let instance2 = new SubType();
  11. console.log(instance2.colors); // ['red', 'blue', 'green', 'yellow']
  12. console.log(instance1.colors === instance2.colors); // true

盗用构造函数继承

(constructor stealing)为了解决原型链继承中如果原型中包含引用值导致的继承问题。
别名 经典继承 / call继承
通过改变 父类 构造函数的 this指向 来达到继承

明确知识点:

  1. 构造函数是一个普通函数,可以当作函数直接调用
  2. 当作普通函数执行时,this指向谁,就在谁身上添加属性

核心代码

在子类构造函数体内执行,父类.call(子类的实例)
创建子类实例时调用 Person 父类构造函数,于是 Student 子类的每一个实例都会讲 Person 父类中的属性复制一份。

  1. function Student(){
  2. // 继承 Person
  3. Person.call(this, 'ebiebi', 16);
  4. }
  1. // 子类
  2. function Student(gender) {
  3. this.gender = gender;
  4. Person.call(this, 'ebiebi', 16); // 核心代码
  5. }
  6. Student.prototype.study = function () { console.log('学习中。。。') }
  7. const StudentInstance = new Student('男');
  8. console.log(StudentInstance);
  9. console.log(StudentInstance instanceof Person); // true

image.png

进阶

  1. // 子类
  2. function Student(gender, name, age) {
  3. this.gender = gender;
  4. Person.call(this, name, age);
  5. }
  6. Student.prototype.study = function () { console.log('学习中。。。') }
  7. const StudentInstance = new Student('女', 'ebiebi', 16);
  8. console.log(StudentInstance);
  9. console.log(StudentInstance instanceof Person); // false

image.png

分析

重点:用 .call().apply() 将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
优点

  1. 解决了原型链继承缺点1、2、3(继承来的属性在自己身上、一个实例化过程在一个位置传参即是可以在子类构造函数中向父类构造函数传参。)
  2. 可以继承多个构造函数属性(call多个)

缺点

  1. 只能继承父类构造函数体内的属性,没有继承父类原型上的属性/方法。
  2. 无法实现复用,每个子类都有父类实例函数的副本,影响性能。

组合继承

组合 原型链 和 盗用构造函数继承 一起使用
基本思路是:使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。

  1. // 子类
  2. function Student(gender, name, age) {
  3. this.gender = gender;
  4. Person.call(this, name, age); // 借用构造函数继承 实例属性
  5. }
  6. Student.prototype = new Person(); // 原型链继承 显示原型上的属性和方法
  7. // Student.prototype.constructor = Student; // 最好加上,解决重写原型导致默认 constructor 丢失的问题
  8. // Student 自己的方法
  9. Student.prototype.study = function () { console.log('学习中。。。') }
  10. const StudentInstance = new Student('女', 'ebiebi', 16);
  11. console.log(StudentInstance);
  12. console.log(StudentInstance instanceof Person); // true

image.png

分析

优点

  1. 父类构造函数体内和原型上的属性都可以继承
  2. 继承下来的属性在自己身上
  3. 一个实例化过程在一个位置传参

缺点

  1. 调用了两次父类构造函数(耗内存),会存在多一份的父类实例属性。
  2. 当你给子类添加方法的时候,实际上是添加在了父类的实例上

(野路子)拷贝继承

别名 for in 继承
利用 for in 循环的特点(不光遍历对象自己,还会遍历proto),来继承所有的属性
直接把父类实例上的所有内容直接复制到子类的 prototype 上

核心代码

  1. // 第一步:先实例化一个父类的实例
  2. const PInstance = new Person(name, age)
  3. // 第二步:使用 for in 循环来遍历这个父类实例
  4. for (let key in PInstance) {
  5. Student.prototype[key] = PInstance[key]
  6. }
  1. function Student(gender, name, age) {
  2. this.gender = gender;
  3. // for in 继承
  4. const PInstance = new Person(name, age)
  5. for (let key in PInstance) {
  6. Student.prototype[key] = PInstance[key]
  7. }
  8. }
  9. Student.prototype.study = function () { console.log('学习中。。。') }
  10. const StudentInstance = new Student('女', 'ebiebi', 16);
  11. console.log(StudentInstance);
  12. console.log(StudentInstance instanceof Person); // false

image.png

分析

优点:

  1. 父类构造函数体内和原型上的属性都可以继承
  2. constructor 正常配套
  3. 添加自己方法的时候,也是正常添加在自己的原型身上

缺点:

  1. for in 循环要一直遍历到 Object.prototype 性能消耗大
  2. 不能继承不可枚举属性

原型式继承

出自2006年,Douglas Crockford 写的一篇文章:《JavaScript中的原型式继承》。文章介绍了一种不涉及严格意义上构造函数的继承方式。出发点是即使不自定义类型也可以通过原型实现 对象之间的信息共享 。
并且给出了如下函数:

核心函数 object()

  1. function object(obj) {
  2. function F() { }
  3. F.prototype = obj;
  4. return new F();
  5. }
  6. // 返回的临时构造函数的实例 为空对象{ } 空对象的 prototype 上有obj的属性

这个 object() 函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的显示原型属性prototype上,然后返回这个临时构造函数的实例。

本质是 object()对传入的对象执行了一次 浅复制 ,将构造函数F 的显示原型属性直接指向了传入的对象。

  1. // 父类
  2. const Person = {
  3. name: 'syukinmei',
  4. age: 23,
  5. idols: ['pdd', 'Wh1t3zZ']
  6. }
  7. const anotherPerson = object(Person);
  8. anotherPerson.name = 'xxx';
  9. anotherPerson.idols.push('Zz1tai');
  10. console.log(anotherPerson)
  11. const yetAnotherPerson = object(Person);
  12. yetAnotherPerson.name = 'ebiebi';
  13. yetAnotherPerson.idols = ['小松菜奈', '鸡你太美']
  14. console.log(yetAnotherPerson);
  15. console.log(Person)

image.png
注意观察 Person 对象的 idols属性被修改了:image.png

这种原型式继承适用于:你有一个对象A,想在他的基础上再创建一个新对象。你需要把A对象先传给 objecy() ,然后再对返回的对象进行适当的修改。
上述例子中,Person 对象定义了另一个对象也应该共享的信息,把它传递给 object() 之后会返回一个新对象。这个新对象的显示原型prototype是 Person ,意味着它的原型上既有原始值属性又有引用值属性。这也意味着 person.idols 不仅是Person的属性,也会跟 anotherPerson 和 yetAnotherPerson 共享。这里实际上克隆了两个Person。

ECMAScript 5 通过增加 Object.create() 方法将原型式继承的概念规范化了。
上述代码种 object() 替换成 Object.create() 结果相同。

分析

优点:

  1. 采用原型式继承并不需要定义一个类,传入参数obj,生成一个继承obj对象的对象。当只想单纯地让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。

缺点:

  1. 不是类式继承,而是原型式基础,缺少了类的概念

寄生式继承

parasitic inheritance 与原型式继承比较接近的一种继承方式,也是 Crockford 首倡的一种模式。
寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个仅仅用于封装继承过程的函数,以某种方式增强对象,然后返回这个对象。

核心函数

  1. function object(obj) {
  2. function F() { }
  3. F.prototype = obj;
  4. return new F();
  5. }
  6. function createAnother(original) {
  7. let clone = object(original); // 通过调用函数创建一个新对象
  8. clone.sayHello = function () { // 以某种方式增强这个对象
  9. console.log(this, 'hello')
  10. }
  11. return clone; // 返回这个对象
  12. }
  13. // clone 对象中不仅具有original所有的属性和方法,而且还有自己的 sayHello 方法

上诉代码中,createAnother() 函数接受一个参数,就是新对象的基准对象。这个对象 original 会被传给 object() 函数,然后将返回的新对象赋值给 clone。接着给 clone 对象添加一个新的方法sayHello() 。最后返回这个对象。

  1. // 父类
  2. let Person = {
  3. name: 'syukinmei',
  4. idols: ['pdd', 'Wh1t3zZ'],
  5. }
  6. let anotherPerson = createAnother(Person);
  7. console.log(anotherPerson);

image.png

分析

优点:

  1. 原型式继承的扩展(其实就是原型式继承得到对象的基础上,在内部再以某种方式来增强对象,然后返回)。

缺点:

  1. 寄生式继承同样主要关注对象,而不是自定义类型和构造函数的情况,依然没有类的概念。

寄生式组合继承

组合 盗用构造函数继承 和 寄生式继承 一起使用

基本思路是:不通过调用父类构造函数给子类的显示原型prototype赋值,而是取得父类显示原型prototype的一个副本。

实现逻辑:使用寄生式继承来获取父类显示原型prototype的属性,然后将返回的对象赋值给子类的显示原型prototype。

核心函数

  1. function object(obj) {
  2. function F() { }
  3. F.prototype = obj;
  4. return new F();
  5. }
  6. function inheritPrototype(subType, superType) {
  7. let prototype = object(superType.prototype); // 创建对象,创建父类显示原型的一个副本
  8. prototype.constructor = subType; // 增强对象,弥补因重写原型而失去的默认 constructor属性
  9. subType.prototype = prototype; // 赋值对象,将新创建的对象赋值给子类的显示原型。
  10. }
  1. // 父类
  2. function Person(name, age) {
  3. this.name = name;
  4. this.age = age;
  5. this.idols = ['pdd', 'Wh1t3zZ']
  6. }
  7. Person.prototype.sayHi = function () { console.log(this.idols, 'Hello World') }
  8. function Student(name, age, gender) {
  9. Person.call(this, name, age); // 盗用构造函数继承 实例属性
  10. this.gender = gender;
  11. }
  12. inheritPrototype(Student, Person); // 寄生式继承 显示原型上的方法
  13. Student.prototype.sayName = function () {
  14. console.log(this.name);
  15. }
  16. const StudentInstance = new Student('syukinmei', 23, '男')
  17. StudentInstance.sayName()
  18. console.log(StudentInstance);

image.png

分析

优点:

  1. 只调用了一次父类构造函数,避免了子类显示原型上不必要也用不到的属性,同时原型链仍然保持不变。

相关文章:

深入JavaScript继承原理

问题:

原型式继承

  1. function object(obj) {
  2. function F() { }
  3. F.prototype = obj;
  4. return new F();
  5. }
  6. // 父类
  7. function Person(name, age) {
  8. this.name = name;
  9. this.age = age;
  10. this.idols = ['pdd', 'wh1t3zZ'];
  11. }
  12. Person.prototype.sayHi = function () { console.log(this.idols, 'Hello World') }
  13. const anotherPerson = object(new Person('syukinmei', 23));
  14. anotherPerson.name = 'xxx';
  15. anotherPerson.idols.push('Zz1tai');
  16. console.log(anotherPerson)
  17. anotherPerson.sayHi()
  18. const yetAnotherPerson = object(new Person('syukinmei', 23));
  19. yetAnotherPerson.name = 'ebiebi';
  20. yetAnotherPerson.idols = ['小松菜奈', '鸡你太美']
  21. console.log(yetAnotherPerson);
  22. yetAnotherPerson.sayHi()
  23. console.log(new Person())

image.png 是不是很完美??