🏕 JavaScript 原生的原型
prototype 属性在 JavaScript 自身的核心部分中被广泛地应用。所有的内建构造函数都用到了它。
# Object.prototype
🌰 例子 / 输出一个空白对象:
let obj = {}
console.log(obj) // [object Object]
2
obj = {}相当于obj = new Object()。其中Object就是一个 内建的对象构造函数,其自身的prototype指向一个带有toString和其他方法的一个巨大的对象。
当 创建一个对象(
new Object()被调用),按照前面的 构造原型,这个新的对象的[[prototype]]属性会被设置为Object.prototype:
所以当 这个新的对象(如上空白对象)要被输出时,调用
obj.toString()方法是来自Object.toString()方法。可以验证:
console.log(obj.__proto__ === Object.prototype) // true console.log(obj.toString === obj.__proto__.toString) // true console.log(obj.toString === Object.prototype.toString) // true1
2
3对于
Object已经是原型链的末端,上方没有更多的[[prototype]]。console.log(Object.prototype.__proto__) // null1
# 其他内建对象的原型
在 JavaScript 中的其他内建对象中,如 Array 、 Date 、 Function 及其他,都在 prototype 上挂载了方法。
🌰 例子 / 数组:
当创建一个数组,会在 内部默认使用 new Array() 构造器。因此, Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。
并且,所有内建原型顶端都是 Object.prototype 。
如下图:

手动验证如下:
let arr = [1, 2, 3]
console.log(arr.__proto__ === Array.prototype) // true
console.log(arr.__proto__.__proto__ === Object.prototype) // true
console.log(arr.__proto__.__proto__.__proto__) // null
2
3
4
5
对于可能重叠的方法,例如 toString() 方法:
🌰 例子:
在数组中 Array.prototype 有自己的 toString 方法列举出数组的所有元素。虽然 Object.prototype 也存在 toString 方法,但是 在原型链中,显然 Array.prototype 更近,所以输出一个数组时,数组对象原型上的方法会被调用。
在浏览器的开发工具可以看到 对象的 原型继承。(内建对象使用 console.dir )
🌰 例子:
对于函数 ,它们是内建构造器 Function 的对象,并且它们的方法( call / apply 及其他)都取自 Function.prototype 。函数也有自己的 toString 方法。
function f() {}
console.log(f.__proto__ === Function.prototype) // true
console.log(f.__proto__.proto === Object.prototype)) // true
2
3
# 基本数据类型的原型
对于 字符串、数字、布尔值,它们并不是对象,如果试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 String 、 Number 和 Boolean 被创建。它们提供给操作字符串、数字和布尔值的方法然后消失。( null 和 undefined 没有对象包装器,他们没有方法和属性,也没有相应的原型)
这些对象是 无形地 创建出来的。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototype 、 Number.prototype 和 Boolean.prototype 进行获取。
# 更改原型原型
🌰 例子 / 向 String.prototype 中添加一个方法,这时这个方法将对所有的字符串都是可用的:
String.prototype.show = function() {
console.log(this);
};
"BOOM!".show(); // BOOM!
2
3
4
5
提示
在开发中不建议将新的方法添加到 原生原型中:原型是全局的,所以很容易造成冲突。如果有两个库都添加了 String.prototype.show 方法,那么其中的一个方法将被另一个覆盖。
🌰 例子 /polyfills 修改内建原型:在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling。
if (!String.prototype.repeat) { // 如果这儿没有这个方法
// 那就在 prototype 中添加它
String.prototype.repeat = function(n) {
// 重复传入的字符串 n 次
// 实际上,实现代码比这个要复杂一些(完整的方法可以在规范中找到)
// 但即使是不够完美的 polyfill 也常常被认为是足够好的
return new Array(n + 1).join(this);
};
}
console.log("La".reapeat(3))
2
3
4
5
6
7
8
9
10
11
12
13
# 从原型中借用
方法借用指 从一个对象中获取一个方法,并将其复制到另一个对象。一些原生的方法通常会被借用。
🌰 例子:
let obj = {
0: 'hello',
1: 'world',
length: 2
}
obj.join = Array.prototype.join
console.log(obj.join(','))
2
3
4
5
6
7
8
上面这个例子,从数组方法中借用了
join,因为obj对象与数组很类似,都有正确的索引和length属性。所以内建方法join可以适用这个对象借用,而不会检查这个对象是不是真正的数组。全面借用数组方法,可以将
obj.__proto__设置为Array.prototype,这样Array中的方法就可以在obj中使用。但是如果
obj已经从另一个对象进行了继承,那么这种方法就不可行了(因为这样会覆盖掉已有的继承。此处obj其实已经从Object进行了继承,但是Array也继承自Object,所以此处的方法借用不会影响obj对原有继承的继承,因为obj通过原型链依旧继承了Object)。因为一次只能继承一个对象。
方法借用很灵活,它允许在需要时混合来自不同对象的方法。
# 总结
所有的内建对象都遵循相同的模式:
- 方法都存储在 prototype 中(例如,
Array.prototype、Object.prototype、Date.prototype等)。 - 对象本身只存储数据(数组元素、对象属性、日期)。
- 方法都存储在 prototype 中(例如,
原始数据类型也将方法存储在包装器对象的 prototype 中:
Number.prototype、String.prototype和Boolean.prototype。只有undefined和null没有包装器对象。内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做。
# 实例
# 给函数的原型添加方法
在所有函数的原型中添加
derfer(ms)方法,该方法将在ms毫秒后运行该函数:function f() { alert("Hello!"); } f.defer(1000);1
2
3
4
5
点击查看
Function.prototype.defer = function(ms) {
setTimeout(this, ms)
}
function f() {
alert("Hello!");
}
f.defer(1000);
2
3
4
5
6
7
8
9
# 给函数的原型添加装饰器方法
在所有函数的原型中添加
defer(ms)方法,该方法返回一个包装器,将函数调用延迟ms毫秒。注意参数应该传给原始函数:function f(a, b) { alert( a + b ); } f.defer(1000)(1, 2);1
2
3
4
5
点击查看
Function.prototype.defer = function(ms) {
let f = this
return function(...args) {
setTimeout(() => f.apply(this, args), ms)
}
}
2
3
4
5
6
在 f.apply 中使用 this 以使装饰器适用于对象方法。
例如:
let user = {
name: "John",
sayHi() {
alert(this.name);
}
}
user.sayHi = user.sayHi.defer(1000);
user.sayHi()
2
3
4
5
6
7
8
9


