# 面向对象-原型和原型链
编程中,我们经常会想获取并扩展一些东西。
例如,我们有一个 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。这就是为什么有人说“一切都从对象继承而来”。
# 基本数据类型
最复杂的事情发生在字符串、数字和布尔值上。
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String
、Number
和 Boolean
(补充还有Bigint
和Symbol
) 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。
值 `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 一切的的终结