🏕 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) // true
1
2
3对于
Object
已经是原型链的末端,上方没有更多的[[prototype]]
。console.log(Object.prototype.__proto__) // null
1
# 其他内建对象的原型
在 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