2022 年 7 月 6 日

原生原型

"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

其他内置原型

其他内置对象(如 ArrayDateFunction 等)也保留原型中的方法。

例如,当我们创建一个数组 [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

基本类型

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

正如我们记得的,它们不是对象。但是,如果我们尝试访问它们的属性,则会使用内置构造函数 StringNumberBoolean 创建临时包装对象。它们提供方法并消失。

这些对象对我们来说是不可见的,大多数引擎都会对它们进行优化,但规范确切地描述了这一点。这些对象的方法也驻留在原型中,可作为 String.prototypeNumber.prototypeBoolean.prototype 使用。

nullundefined没有对象包装器

特殊值nullundefined是独立存在的。它们没有对象包装器,因此无法使用它们的方法和属性。也没有相应的原型。

更改原生原型

原生原型可以修改。例如,如果我们向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.prototypeObject.prototypeDate.prototype 等)。
    • 对象本身只存储数据(数组项、对象属性、日期)。
  • 基本类型也把方法存储在包装对象的原型中:Number.prototypeString.prototypeBoolean.prototype。只有 undefinednull 没有包装对象。
  • 可以修改内置原型或使用新方法填充它们。但建议不要更改它们。唯一允许的情况可能是当我们添加一个新标准时,但 JavaScript 引擎还不支持它。

任务

重要性:5

向所有函数的原型添加方法 defer(ms),它在 ms 毫秒后运行函数。

在完成之后,以下代码应该可以工作

function f() {
  alert("Hello!");
}

f.defer(1000); // shows "Hello!" after 1 second
Function.prototype.defer = function(ms) {
  setTimeout(this, ms);
};

function f() {
  alert("Hello!");
}

f.defer(1000); // shows "Hello!" after 1 sec
重要性:4

向所有函数的原型添加方法 defer(ms),它返回一个包装器,延迟 ms 毫秒调用。

以下是如何工作的示例

function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // shows 3 after 1 second

请注意,参数应传递给原始函数。

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

// check it
function f(a, b) {
  alert( a + b );
}

f.defer(1000)(1, 2); // shows 3 after 1 sec

请注意:我们在 f.apply 中使用 this 使我们的装饰器适用于对象方法。

因此,如果包装器函数被调用为对象方法,则 this 会传递给原始方法 f

Function.prototype.defer = function(ms) {
  let f = this;
  return function(...args) {
    setTimeout(() => f.apply(this, args), ms);
  }
};

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

user.sayHi = user.sayHi.defer(1000);

user.sayHi();
教程地图

评论

在评论之前阅读此内容…
  • 如果你有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果你无法理解文章中的某些内容 - 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码,请将其包装在 <pre> 标记中,对于 10 行以上的代码,请使用沙盒(plnkrjsbincodepen…)