目录

🏕 JavaScript 原生的原型

prototype 属性在 JavaScript 自身的核心部分中被广泛地应用。所有的内建构造函数都用到了它。

# Object.prototype

🌰 例子 / 输出一个空白对象

let obj = {}
console.log(obj) // [object Object]
1
2

obj = {} 相当于 obj = new Object() 。其中 Object 就是一个 内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。

image-20220514130153731

当 创建一个对象( new Object() 被调用),按照前面的 构造原型,这个新的对象的 [[prototype]] 属性会被设置为 Object.prototype

image-20220514130858908

所以当 这个新的对象(如上空白对象)要被输出时,调用 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 中的其他内建对象中,如 ArrayDateFunction 及其他,都在 prototype 上挂载了方法。

🌰 例子 / 数组:

当创建一个数组,会在 内部默认使用 new Array() 构造器。因此, Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。

并且,所有内建原型顶端都是 Object.prototype

如下图:
image-20220514131340973

手动验证如下:

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
1
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
1
2
3

# 基本数据类型的原型

对于 字符串、数字、布尔值,它们并不是对象,如果试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean 被创建。它们提供给操作字符串、数字和布尔值的方法然后消失。( null undefined 没有对象包装器,他们没有方法和属性,也没有相应的原型)

这些对象是 无形地 创建出来的。这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototypeNumber.prototypeBoolean.prototype 进行获取。

# 更改原型原型

🌰 例子 / 向 String.prototype 中添加一个方法,这时这个方法将对所有的字符串都是可用的:

String.prototype.show = function() {
  console.log(this);
};

"BOOM!".show(); // BOOM!
1
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))
1
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(','))
1
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.prototypeObject.prototypeDate.prototype 等)。
    • 对象本身只存储数据(数组元素、对象属性、日期)。
  • 原始数据类型也将方法存储在包装器对象的 prototype 中: Number.prototypeString.prototypeBoolean.prototype 。只有 undefinednull 没有包装器对象。

  • 内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 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);
1
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)
  }
}
1
2
3
4
5
6

f.apply 中使用 this 以使装饰器适用于对象方法。

例如:

let user = {
  name: "John",
  sayHi() {
    alert(this.name);
  }
}

user.sayHi = user.sayHi.defer(1000);
user.sayHi()
1
2
3
4
5
6
7
8
9
📢 上次更新: 2022/09/02, 10:18:16