原型链和继承

一、关键字

  • 对象 object:是数据的集合
  • 原型继承
  • 构造器
  • 原型链
  • 子类继承

JavaScript 中的(几乎)一切都是对象,除了原始值以外(number,string,boolean,undefined,null)

二、经典继承 vs 原型继承

编程中的继承意味着一个对象基于另一个对象,并且可以访问其他对象的属性和方法。

JavaScript 使用原型继承,它不像 C 或者 Java 那样的经典继承,这是一个重要的区别。

经典继承 原型继承
class prototype
对象实例从 class 继承 对象实例从其他对象继承
子 class 可以从父 class 继承 不从 class 继承(因为 JS 没有 class 的概念,即使是 ES6 的 class,它的底层也是基于 prototype,所以它只是一个类似经典 class 的语法糖)
class 是不可变的,它不能在运行时更改 原型prototype可以在运行时更改
类支持或不支持多重继承(取决于语言) 对象可以从多个原型继承

原型继承

  • 原型继承的定义特征是对象实例可以通过原型链访问继承的属性和方法。

  • 这是通过 JavaScript 中的每个对象自动赋予一个原型属性prototype来实现的。

  • 当创建继承自父对象的子对象时,子对象可以访问父对象的原型属性prototype

  • proptotype 属性本身是一个对象,并且因为子对象可以访问其父对象的原型属性,因此子对象可以访问存储在此处(prototype)的任何属性和方法。

案例,假设有一个 vehicle(车辆) 对象,在许多其他语言中,Vehicle会被认为是一个class,因为它是创建vehicle(车辆)类型实例的蓝图。

然而 JavaScript 并没有 class 的概念,取而代之的是一个构造函数(对象),用作创建其他对象的蓝图。

- - - step1. 构建一个“类”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

// 使用构造器(构造函数)构建一个`Vehicle(车辆)`类
let Vehicle = function (color, weight, year) {
  this.color = color;
  this.weight = weight;
  this.year = year;
  this.honk = function () {
    console.log("Honk!"); //鸣喇叭
  };
};

- - - step2. 实列化一个对象

1
2
3
4

let myVehicle = new Vehicle('blue', '500', 2019);	
myVehicle.color(); // 'blue'
myVehicle.honk(); // 'Honk!'

- - - step3. 原型 prototype 登场 - 优化

在上例中,因为已经在构造函数体中定义了 honk 方法,

所以每个新的车辆实例都会有它自己单独的 honk 副本存储在内存中。

由于 honk 始终是相同的功能,因此每个实例存储自己的唯一副本是没有意义的。

这就是原型的力量发挥作用的地方。(对象可以访问其父对象的原型属性,因此将 honk 方法存储在 Vehicle 的原型属性中,则所有实例都可以访问它,而无需存储自己的副本。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

let Vehicle = function (color, weight, year) {
  this.color = color;
  this.weight = weight;
  this.year = year;
};

Vehicle.prototype.honk = function () {
  console.log("Honk!");
};

let myVehicle = new Vehicle("blue", "500", 2019);

myVehicle.honk(); // 'Honk!'

三、原型链 __proto__ (链接)

本质上,当调用 honk 方法时,JavaScript 将查看调用对象的属性,在本例中为 myVehicle,用于名为 honk 的方法(或属性)。 如果在对象上找不到此属性,我们将向上移动一级原型链。

为了建立对象与其原型之间的链接,使用了 myVehicle 的 proto 属性。 这个 proto 属性本质上是指向它的构造函数原型的链接。

一般而言,实例的 __proto__ 属性是指向它的原型(其构造函数的原型属性)的链接

在控制台中演示:

1
2

myVehicle.__proto__ === Vehicle.prototype // true

因为在原型上显式地存储了 honk 方法,所以,Vehicle 的所有实例都可以访问 honk 方法,而无需存储自己的副本。

四、子类继承(用原型链模拟)

  • fn.call():call方法的作用是调用fn构造函数

  • Object.create():允许创建一个新对象,同时指定该对象的原型

假设我们有一个不同的构造函数,它类似于 Vehicle(车辆) 但更具体,例如 Motorcycle(摩托车) 构造函数。

Motorcycle(摩托车) 具有与 Vehicle(车辆) 相同的所有属性和功能,但它还具有 engineSize(引擎尺寸) 属性和加油方法。

因为摩托车与车辆共享相同的属性,所以可以使用继承来简化代码。 通过让 MotorcycleVehicle 继承,可以避免两次指定共享的属性和方法。

  • 注意:类和子类是经典继承的一个特征,而不是原型继承,这意味着Motorcycle实际上不是一个子类。 在 JavaScript 中,使用原型链来模拟类和子类。
1
2
3
4
5
6
7
8
9

let Motorcycle = function (color, weight, year, engineSize) {
  Vehicle.call(this, color, weight, year); //call方法的作用是调用Vehicle构造函数
  this.engineSize = engineSize;
};

Motorcycle.prototype = Object.create(Vehicle.prototype) //Object.create方法允许创建一个新对象,同时指定该对象的原型

let myMotorcycle = new Motorcycle("green", 150, 2015, 500);

通过这一步,已经连接了原型链。

Motorcycle.prototype 的 proto 属性将设置为 Vehicle.prototype,摩托车实例现在可以访问摩托车原型以及车辆原型上存储的所有属性和方法。

为了演示这一点,在 Motorcycles 原型中添加加油方法:

1
2
3
4
5
6
7

Motorcycle.prototype.refuel = function () {
  console.log("Your fuel tank is now full!");
};

myMotorcycle.honk(); // 'Honk!'
myMotorcycle.refuel(); // 'Your fuel tank is now full!'

五、结语

JavaScript 实现面向对象编程的方式与许多其他使用基于class类的语言不同。

通过掌握原型继承、构造函数和原型链,才可以充分利用语言特性并了解 JavaScript 在底层是如何工作的。