"prototype"
属性被 JavaScript 自身广泛使用。所有内置构造函数都使用它。
首先,我们来看看细节,然后看看如何使用它为内置对象添加新功能。
Object.prototype
假设我们输出一个空对象
let obj = {};
alert( obj ); // "[object Object]" ?
生成字符串 "[object Object]"
的代码在哪里?这是一个内置的 toString
方法,但它在哪里?obj
是空的!
…但是简短表示法 obj = {}
与 obj = new Object()
相同,其中 Object
是一个内置的对象构造函数,它自己的 prototype
引用一个包含 toString
和其他方法的巨大对象。
以下是发生的情况
当调用 new Object()
(或创建一个字面对象 {...}
)时,它的 [[Prototype]]
根据我们在上一章中讨论的规则设置为 Object.prototype
因此,当调用 obj.toString()
时,该方法将从 Object.prototype
中获取。
我们可以这样检查它
let obj = {};
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
等)也保留原型中的方法。
例如,当我们创建一个数组 [1, 2, 3]
时,内部使用默认的 new Array()
构造函数。因此,Array.prototype
成为其原型并提供方法。这非常节省内存。
根据规范,所有内置原型都在顶部有 Object.prototype
。这就是为什么有些人说“一切都是从对象继承的”。
以下是总体情况(适合 3 个内置对象)
让我们手动检查原型
let arr = [1, 2, 3];
// it inherits from Array.prototype?
alert( arr.__proto__ === Array.prototype ); // true
// then from Object.prototype?
alert( arr.__proto__.__proto__ === Object.prototype ); // true
// and null on the top.
alert( arr.__proto__.__proto__.__proto__ ); // null
原型中的一些方法可能重叠,例如,Array.prototype
有自己的 toString
,它列出逗号分隔的元素
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- the result of Array.prototype.toString
正如我们之前看到的,Object.prototype
也有 toString
,但 Array.prototype
在链中更近,因此使用数组变体。
Chrome 开发者控制台等浏览器内工具也显示继承(对于内置对象可能需要使用 console.dir
)
其他内置对象也以相同的方式工作。即使是函数——它们也是内置 Function
构造函数的对象,并且它们的方法(call
/apply
等)取自 Function.prototype
。函数也有自己的 toString
。
function f() {}
alert(f.__proto__ == Function.prototype); // true
alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects
基本类型
最复杂的事情发生在字符串、数字和布尔值上。
正如我们记得的,它们不是对象。但是,如果我们尝试访问它们的属性,则会使用内置构造函数 String
、Number
和 Boolean
创建临时包装对象。它们提供方法并消失。
这些对象对我们来说是不可见的,大多数引擎都会对它们进行优化,但规范确切地描述了这一点。这些对象的方法也驻留在原型中,可作为 String.prototype
、Number.prototype
和 Boolean.prototype
使用。
null
和undefined
没有对象包装器特殊值null
和undefined
是独立存在的。它们没有对象包装器,因此无法使用它们的方法和属性。也没有相应的原型。
更改原生原型
原生原型可以修改。例如,如果我们向String.prototype
添加一个方法,则所有字符串都可以使用该方法
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
在开发过程中,我们可能会想到想要的新内置方法,并且可能会尝试将它们添加到原生原型中。但这通常不是一个好主意。
原型是全局的,因此很容易发生冲突。如果两个库都添加了一个方法String.prototype.show
,那么其中一个库将覆盖另一个库的方法。
因此,通常情况下,修改原生原型被认为是一个坏主意。
在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfill。
Polyfill 是指为 JavaScript 规范中存在但特定 JavaScript 引擎尚不支持的方法制作替代品。
然后,我们可以手动实现它,并用它填充内置原型。
例如
if (!String.prototype.repeat) { // if there's no such method
// add it to the prototype
String.prototype.repeat = function(n) {
// repeat the string n times
// actually, the code should be a little bit more complex than that
// (the full algorithm is in the specification)
// but even an imperfect polyfill is often considered good enough
return new Array(n + 1).join(this);
};
}
alert( "La".repeat(3) ); // LaLaLa
从原型中借用
在章节 装饰器和转发、call/apply 中,我们讨论了方法借用。
这是指我们从一个对象中获取一个方法并将其复制到另一个对象中。
原生原型的某些方法经常被借用。
例如,如果我们正在制作一个类似数组的对象,我们可能希望将一些Array
方法复制到其中。
例如
let obj = {
0: "Hello",
1: "world!",
length: 2,
};
obj.join = Array.prototype.join;
alert( obj.join(',') ); // Hello,world!
之所以有效,是因为内置join
方法的内部算法只关心正确的索引和length
属性。它不会检查对象是否确实是数组。许多内置方法都是这样的。
另一种可能性是通过将obj.__proto__
设置为Array.prototype
来继承,这样所有Array
方法都会自动在obj
中可用。
但如果obj
已经从另一个对象继承,则这是不可能的。请记住,我们一次只能从一个对象继承。
借用方法很灵活,它允许在需要时混合来自不同对象的功能。
总结
- 所有内置对象都遵循相同的模式
- 这些方法存储在原型中(
Array.prototype
、Object.prototype
、Date.prototype
等)。 - 对象本身只存储数据(数组项、对象属性、日期)。
- 这些方法存储在原型中(
- 基本类型也把方法存储在包装对象的原型中:
Number.prototype
、String.prototype
和Boolean.prototype
。只有undefined
和null
没有包装对象。 - 可以修改内置原型或使用新方法填充它们。但建议不要更改它们。唯一允许的情况可能是当我们添加一个新标准时,但 JavaScript 引擎还不支持它。
评论
<code>
标记,对于多行代码,请将其包装在<pre>
标记中,对于 10 行以上的代码,请使用沙盒(plnkr、jsbin、codepen…)