JavaScript中的继承

2021/6/16 JavaScript

# 关系分析

// 父类
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  };
}
Person.prototype.getAge = function () {
  return this.age;
};
const person = new Person('fcq', 20);
1
2
3
4
5
6
7
8
9
10
11
12
  1. __proto__prototype是什么联系

    如果有一个实例,它是由一个构造函数实例而来,那么这个实例的__proto__一定指向这个构造函数的prototype,即person.__proto__ === Person.prototype

  2. prototypeconstructor是什么联系

    constructor就是某个普通构造函数的prototype自身的一个属性(用hasOwnProperty可验证),它指向的就是这个构造函数本身,即Person.prototype.constructor === Person

    • Function.prototype.hasOwnProperty('constructor'); // true
    • Object.prototype.hasOwnProperty('constructor'); // true
  3. __proto__constructor是什么联系

    __proto__constructor的联系跟prototypeconstructor的联系一样。因为以.__proto__结尾的,它最后一定指向某个构造函数的原型对象(Person.prototype),然后又由于constructor是某个构造函数的prototype自身的一个属性,因此我们可以这么看:person.__proto__.constructor === Person.prototype.constructor

总结

  1. __proto__constructor属性是对象所独有的;prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有__proto__constructor属性。
  2. __proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值,会报错。通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
  3. prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即person.__proto__ === Person.prototype
  4. constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function
image-default

判断方法:

  1. 如果最后以.__proto__结尾,它最后返回的一定是某个构造函数的prototypeObject.prototype.__proto__除外,它到顶了,是原型链的顶端,返回null)(顶级构造函数的构造函数都是Function,例如Function.__proto__ === Function.prototype)。

  2. 如果是以.prototype结尾,那么它前面一定是个构造函数,因为只有函数才会有prototype属性,并且任何函数的prototype都会自动生成一个constructor的函数,指向本身(以.prototype结尾返回的都是这个构造函数的prototype所有的方法与属性)。

  3. 如果是以.constructor结尾,先弄清楚前面是什么:

    • 如果前面是实例,那它直接返回创造实例的那个构造函数。

    • 如果前面直接是顶级基类构造函数(Function.constructor)或者是普通构造函数(Person.constructor),它会直接指向构造所有函数的顶级基类构造函数Function(所有构造函数都是函数,都由顶级构造函数Function而来,所以constructor指向它。

    • 如果前面是构造函数的原型对象(Person.prototype.constructor),因为实例的constructor是继承自构造函数.prototype.constructor,所以构造函数.prototype.constructor必须指回它自己,(构造函数.prototype.constructor === 构造函数)。针对这点,我们看看它是怎么继承来的:

      • constructor整个继承的流程是:在实例person本身查找,找不到去person.__proto__Person.prototype)找,发现有Person.prototype.constructor,并且Person.prototype.constructor === Person返回它,所以person.constructor === Person

原文 (opens new window)

# 原型链继承





 
 




function Inherit1() {
  this.name = 'Inherit1';
}

Inherit1.prototype = new Person('Inherit1', 20); // 实现核心
Inherit1.prototype.constructor = Inherit1;

const inherit1 = new Inherit1();
console.log(inherit1 instanceof Person); // true
1
2
3
4
5
6
7
8
9

TIPS

  1. 实现

    • 让新实例的原型等于父类的实例。
  2. 特点

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

    • 新实例无法向父类构造函数传参。
    • 继承单一。
    • 所有新实例都会共享父类实例的属性(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改)。

# 构造函数继承


 
 





function Inherit2(name, age) {
  Person.call(this, name, age); // 实现核心
  // Person.apply(this, arguments)
  this.age = age;
}
const inherit2 = new Inherit2('Inherit2', 30);
console.log(inherit2 instanceof Person); // false
1
2
3
4
5
6
7

TIPS

  1. 实现

    • .call().apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行)。
  2. 特点

    • 只继承了父类构造函数的属性,没有继承父类原型的属性。
    • 解决了原型链继承缺点。
    • 可以继承多个构造函数属性(call多个)。
    • 在子实例中可向父实例传参。
  3. 缺点

    • 只能继承父类构造函数的属性。
    • 无法实现构造函数的复用(每次用每次都要重新调用)。
    • 每个新实例都有父类构造函数的副本,臃肿。

# 组合继承

组合原型链继承借用构造函数继承(常用)。


 







function Inherit3(name, age) {
  Person.call(this, name, age); // 实现核心
  this.age = age;
}
Inherit3.prototype = new Person();
Inherit3.prototype.constructor = Inherit3;

const inherit3 = new Inherit3('Inherit3', 40);
1
2
3
4
5
6
7
8

TIPS

  1. 实现

    • 结合了两种模式的优点,传参和复用。
  2. 特点:

    • 可以继承父类原型上的属性,可以传参,可复用。
    • 每个新实例引入的构造函数属性是私有的。
  3. 缺点

    • 调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

# 原型式继承

function inheritObject() {
  // 声明一个过渡对象
  function F() {}
  // 过渡对象的原型继承父类
  F.prototype = new Person('bob', 20);
  // 返回过渡对象的实例,该实例的原型继承了父类对象
  return new F();
}
1
2
3
4
5
6
7
8

TIPS

  1. 实现

    • 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。Object.create()就是这个原理。
  2. 特点

    • 类似于复制一个对象,用函数来包装。
  3. 缺点

    • 所有实例都会继承原型上的属性。
    • 无法实现复用(新实例属性都是后面添加的)。

# 寄生式继承

function inheritObject(o) {
  // 声明一个过渡对象
  function F() {}
  // 过渡对象的原型继承父类
  F.prototype = o;
  // 返回过渡对象的实例,该实例的原型继承了父类对象
  return new F();
}
function createInherit(obj) {
  // 通过原型式继承创建对象
  const o = inheritObject(obj);
  o.sayName = function () {
    console.log(this.name);
  };
  // 返回自定义扩展之后的对象
  return o;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

TIPS

其实寄生式继承就是对原型式继承的二次封装,在封装过程中进行了扩展。

  1. 实现

    • 就是给原型式继承外面套了个壳子。
  2. 优点

    • 没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
  3. 缺点

    • 没用到原型,无法复用。

# 寄生式组合继承

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

function inheritPrototype(Child, Parent) {
  const prototype = inheritObject(Parent.prototype);
  prototype.constructor = Child;
  Child.prototype = prototype;
}

// 上面的写法可以替换为下面这种
// function inheritPrototype(Child, Parent) {
//   Child.prototype = Object.create(Parent.prototype)
//   // 又或者
//   // Child.prototype.__proto__ = Parent.prototype
//   Child.prototype.constructor = Child;
// }

function Inherit4(name, age) {
  Person.call(this, name, age);
  this.age = age;
}

inheritPrototype(Inherit4, Person);

const inherit4 = new Inherit4('inherit4', 50);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

TIPS

  • 寄生:在函数内返回对象然后调用。

  • 组合:

    • 函数的原型等于另一个实例。
    • 在函数中用apply或者call引入另一个构造函数,可传参。

# ES6的Class继承

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayName() {
    console.log(this.name);
  }
}

class Child extends Person {
  constructor(name, age, gender) {
    super(name, age);
    this.gender = gender;
  }
  getAge() {
    return this.age;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIPS

子类中的this是通过父类创建好this后传递下来的,所以子类必须要先调用super()

ES6里用的是绝对的概念:基类派生类。而且这个概念是针对所有构造函数说的,JS里的构造函数要么是基的,要么就是派生的。

ES6中,我们随手写的构造函数(function)都是基类,基类可以直接用this来指向调用它所在方法的对象。

参考文章:

最近更新: 2024年10月30日 22:08:01