prototype”属性被 JavaScript 语言核心广泛使用。所有内置的构造函数都使用了它。

本章我们将学习到里面的详细细节,然后再介绍如何使用它为内置对象添加新功能。

Object.prototype

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

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

结果打印出字符串 "[object Object]"?这就是内置的 toString 方法在起作用,但它在哪儿呢?obj 可是空的啊!

其实,简写语法 obj = {} 等同于 obj = new Object()。这里的 Object 是内置的对象构造器函数,这个函数上的 prototype 属性,引用了一个非常巨大的对象(包括 toString 方法在内的许多其它方法包含其中)。

我们看看发生了什么:

原生原型对象 - 图1

调用 new Object()(或用对象字面量形式 {...})创建了一个对象,这个新创建对象的 [[Prototype]] 就会设置到 Object.prototype

原生原型对象 - 图2

因此,我们调用的 obj.toString() 方法,实际使用的 Object.prototype 对象上的 toString 方法 。

我们来测一下:

  1. let obj = {};
  2. alert( obj.__proto__ === Object.prototype ); // true
  3. // obj.toString === obj.__proto__.toString === Object.prototype.toString

值得注意的是,Object.prototype 是没有 __proto__(即 [[Prototype]])属性的。

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

image.png

Object.prototype 上没有 __proto__ 属性

其他的内置原型

其他诸如 ArrayDateFunction 的内置对象也在各自原型上提供了自己的方法。

举个例子:我们创建了一个数组 [1, 2, 3],内部调用的就是 new Array() 构造函数。因此,对数组对象而言,Array.prototype 是它的原型,同时提供了特定于数组的方法。这是方式也是非常节省内存的。

根据规范,所以内置原型的顶部都是 Object.prototype。这就是为何有些人会说“一切都继承自对象”的原因。

这里有一个整体图可供参考(包含 3 个内置对象):

原生原型对象 - 图4

我们手动检查下原型看看:

  1. let arr = [1, 2, 3];
  2. // 是继承自 Array.prototype?
  3. alert( arr.__proto__ === Array.prototype ); // true
  4. // 然后继承自 Object.prototype?
  5. alert( arr.__proto__.__proto__ === Object.prototype ); // true
  6. // 然后最顶上就是 null 了
  7. alert( arr.__proto__.__proto__.__proto__ ); // null

原型里的一些方法可能会被覆盖。例如,Array.prototype 定义了自己的 toString 方法,返回用逗号分隔的字符串:

  1. let arr = [1, 2, 3]
  2. alert(arr); // 1,2,3 <-- 这里是调用 Array.prototype.toString 方法返回的结果

正如之前看到的那样,Object.prototype 上也有 toString 方法,但 Array.prototype 在原型链中距离较近,因此被数组对象使用了。

原生原型对象 - 图5

在 Chrome 浏览器的开发者工具栏中,也能看到继承关系:

原生原型对象 - 图6

其他内置对象也是同样的原理。甚至是函数——函数是使用内置的 Function 构造函数创建的对象,它的方法(call/apply 和等其他)是从 Function.prototype 继承的。函数也有自己的 toString 方法。

  1. function f() {}
  2. alert(f.__proto__ == Function.prototype); // true
  3. alert(f.__proto__.__proto__ == Object.prototype); // true, 继承自对象

原始类型

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

我们知道,这些都不是对象。当我们在它们身上访问属性时,一个临时的包装对象会被创建,使用的是对应的内置构造函数(即指 StringNumberBolean),访问结束后,包装对象就被销毁了。

这些对象对我们是不可见的,大多数引擎会优化它们的行为,但规范如是描述。这些对象的方法也存在于原型中,使用的也是 String.prototypeNumber.prototypeBoolean.prototype 对象上的方法。

注意:**nullundefined 没有对应的包装对象**

特殊值 nullundefined 要分开讨论。他们都没有对应的包装对象,因此无法访问它们的属性和方法,它们也没有对应的原型对象。

修改原生原型

原生原型可以被修改。比如,如果我们给 String.prototype 添加了一个方法,那么所有的字符串都能使用这个方法了:

  1. String.prototype.show = function() {
  2. alert(this);
  3. };
  4. "BOOM!".show(); // BOOM!

实际开发中,我们可能会有向原生原型对象中,添加新方法的冲动,但这样做通常不是个好主意。

重要提示: ** 原型是全局的,因此很容易产生冲突。如果两个依赖库都在 String.prototype 添加了 .show 这个方法,那势必其中一个会被另一个覆盖。

因此,直接修改原生原型对象扩展方法的方式通常不是个好主意。

只有一种情况是可以修改原生原型的,就是在写 polyfills 的时候。

Polyfills 是一个术语。如果某些 JavaScript 引擎因为各种原因没有支持当前规范中定义的内置方法或属性,那么就可以通过 Polyfills 的形式,手动在原生原型上添加。

举个例子:

  1. if (!String.prototype.repeat) { // 如果没有这个方法
  2. // 就把这个方法添加到原型上
  3. String.prototype.repeat = function(n) {
  4. // 重复字符串 n 次
  5. // 实际代码比这里复杂,
  6. // 对“n”为负值的情况抛出错误
  7. // 完整的算法过程在规范里描述了
  8. return new Array(n + 1).join(this);
  9. };
  10. }
  11. alert( "La".repeat(3) ); // LaLaLa

从原型借方法使用

这是在我们从一个对象获取一个方法并将其复制到另一个对象时会用到的形式。

通常会借用一些原生原型的方法。

假设我们有一个类数组对象,就可能需要将一些 Array 上的方法复制到该对象。

举个例子:

  1. let obj = {
  2. 0: "Hello",
  3. 1: "world!",
  4. length: 2,
  5. };
  6. obj.join = Array.prototype.join;
  7. alert( obj.join(',') ); // Hello,world!

这样做之所以可行,是因为 join 方法内部的算法并关系操作的对象是不是数组,只要有正确的索引和 length 属性就可以。许多的内置对象都像这样的。

另一种实现方式,是将 obj.__proto__ 的值直接设置成 Array.prototype,这样的话,所有的 Array 方法都可以在 obj 中获得。

当然了,如果 obj 已经继承了一个对象,这样做就不行。因为,一个对象同一时间只能有一个原型对象。

借用方法还是挺灵活的,我们可以在需要时混合来自不同对象的功能。

总结

  • 所有的内置对象遵循同一个模式:

    • 方法存储在原型里(Array.prototypeObject.prototypeDate.prototype 等)。

    • 对象本身只存储数据(数组成员、对象属性、日期等)。

  • 原始类型值也将方法存储在包装对象的原型中:Number.prototypeString.prototypeBoolean.prototype。只有 undefinednull 是没有对应的包装对象的。

  • 内置原型可以被修改,或者用新的方法填充,但通常并不推荐这么做。但有一种情况是可以修改原生原型的,就是在写 polyfills 的时候。

(完)


📄 文档信息

🕘 更新时间:2020/01/15
🔗 原文链接:http://javascript.info/native-prototypes