# 面向对象-原型和原型链

编程中,我们经常会想获取并扩展一些东西。

例如,我们有一个 user 对象及其属性和方法,并希望将 admin 和 guest 作为基于 user 稍加修改的变体。我们想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象。

原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求。

# 原型指针

# [[Prototype]]

  • 正式规范

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的),它要么为 null,要么就是对另一个对象的引用。该对象被称为“原型”。

一个对象只能有一个 [[Prototype]]。一个对象不能从其他两个对象获得继承。

# __proto__

  • 历史遗留

__proto__[[Prototype]] 的因历史原因而留下来的 getter/setter。

TIP

方便起见,本文后面统一使用__proto__来进行描述。

let animal = {
  eats: true,
  walk() {
    alert("Animal walk");
  }
};
let rabbit = {
  jumps: true,
  __proto__: animal
};
// walk 方法是从原型中获得的
rabbit.walk(); // Animal walk

TIP

__proto__ 与内部的 [[Prototype]] 不一样。__proto__[[Prototype]]getter/setter。稍后,我们将看到在什么情况下理解它们很重要,在建立对 JavaScript 语言的理解时,让我们牢记这一点。

__proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型。 根据规范,__proto__ 必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。

# 原型对象

# F.prototype

我们还记得,可以使用诸如 new F() 这样的构造函数来创建一个新对象。

如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 Prototype

TIP

JavaScript 从一开始就有了原型继承。这是 JavaScript 编程语言的核心特性之一。

但是在过去,没有直接对其进行访问的方式。唯一可靠的方法是本章中会介绍的构造函数的 "prototype" 属性。目前仍有许多脚本仍在使用它。

显式的修改函数的prototype

let animal = {
  eats: true
};
function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal
alert(rabbit.eats); // true

# constructor

每个函数都有 "prototype" 属性,即使我们没有提供它。

默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

alert(Rabbit.prototype.constructor == Rabbit); // true

通常,如果我们什么都不做,constructor 属性可以通过 [[Prototype]] 给所有 rabbits 使用

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}

alert(rabbit.constructor == Rabbit); // true (from prototype)

# Native 的原型

Native Object 指宿主环境本身提供的那些对象

# Object.prototype

假如我们输出一个空对象:

let obj = {};
alert(obj); // "[object Object]" ?

生成字符串 "[object Object]" 的代码在哪里?那就是一个内建的 toString 方法,但是它在哪里呢?obj 是空的! obj = {}obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。

验证代码如下

let obj = {};
alert(obj); // "[object Object]"
alert(obj.__proto__ === Object.prototype); // true

alert(obj.toString === obj.__proto__.toString); //true
alert(obj.toString === Object.prototype.toString); //true

请注意在 Object.prototype 上方的链中没有更多的 [[Prototype]]

alert(Object.prototype.__proto__); // null

# 其他内建原型

其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。

例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。

按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。

# 基本数据类型

最复杂的事情发生在字符串、数字和布尔值上。

正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean (补充还有BigintSymbol) 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。

值 `null` 和 `undefined` 没有对象包装器

特殊值 null 和 undefined 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型。

# 修改原生原型

在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法。 在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfill

# 总结

所有的内建对象都遵循相同的模式(pattern):

  • 方法都存储在 prototype 中(Array.prototype、Object.prototype、Date.prototype 等)。
  • 对象本身只存储数据(数组元素、对象属性、日期)。

原始数据类型也将方法存储在包装器对象的 prototype 中:Number.prototype、String.prototype 和 Boolean.prototype。只有 undefined 和 null 没有包装器对象。

内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做。

# 原型方法

__proto__ 被认为是过时且不推荐使用的(deprecated),这里的不推荐使用是指 JavaScript 规范中规定,__proto__ 必须仅在浏览器环境下才能得到支持。

一般不推荐直接使用__proto__来修改原型,现代的方法有:

Object.create(proto, [descriptors]);
Object.getPrototypeOf(obj);
Object.setPrototypeOf(obj, proto);
let animal = {
  eats: true
};

// 创建一个以 animal 为原型的新对象
let rabbit = Object.create(animal);
alert(rabbit.eats); // true
alert(Object.getPrototypeOf(rabbit) === animal); // true
Object.setPrototypeOf(rabbit, {}); // 将 rabbit 的原型修改为 {}

我们可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:

let clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

# 原型链溯源图

  • 规则 1: 一切构造函数的默认原型通过 Object 来创建,可以通过修改原型改变继承关系
  • 规则 2: Native Function 一切的开始
  • 规则 3: null 一切的的终结