Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ importance: 5

---

# 给函数添加一个方法 “f.defer(ms)
# 给函数添加一个 "f.defer(ms)" 方法

为所有函数的原型添加 `defer(ms)` 方法,能够在 `ms` 毫秒后执行函数
在所有函数的原型中添加 `defer(ms)` 方法,该方法将在 `ms` 毫秒后运行该函数

当你完成添加后,下面的代码应该是可执行的:

Expand All @@ -13,5 +13,5 @@ function f() {
alert("Hello!");
}

f.defer(1000); // 1 秒后显示 Hello!
f.defer(1000); // 1 秒后显示 "Hello!"
```
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,29 @@ function f(a, b) {
alert( a + b );
}

f.defer(1000)(1, 2); // shows 3 after 1 sec
f.defer(1000)(1, 2); // 1 秒后显示 3
```

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

因此,如果将包装器函数作为对象方法调用,那么 `this` 将会被传递给原始方法 `f`。

```js run
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();
```
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ importance: 4

---

# 添加装饰器方法 “defer()” 到函数
# 将装饰者 "defer()" 添加到函数

添加方法 `defer(ms)` 到所有的函数原型,它返回一个包装函数,延迟 `ms` 毫秒调用函数
在所有函数的原型中添加 `defer(ms)` 方法,该方法返回一个包装器,将函数调用延迟 `ms` 毫秒

这里是它应该如何执行的例子
下面是它应该如何执行的例子

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

f.defer(1000)(1, 2); // 1 秒钟后显示 3
f.defer(1000)(1, 2); // 1 秒后显示 3
```

请注意参数应该被传给原函数
请注意,参数应该被传给原始函数
125 changes: 69 additions & 56 deletions 1-js/08-prototypes/03-native-prototypes/article.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
# 原生的原型

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

我们将会首先看到原型对于简单对象是什么样的,然后对于更多的复杂对象又是什么样的
首先,我们将看看原生原型的详细信息,然后学习如何使用它为内建对象添加新功能

## Object.prototype

假设我们输出一个空对象
假如我们输出一个空对象

```js run
let obj = {};
alert( obj ); // "[object Object]" ?
```

生成字符串 `"[object Object]"` 的代码在哪里?那就是内置的一个 `toString` 方法,但是它在哪里呢?`obj` 是空的!
生成字符串 `"[object Object]"` 的代码在哪里?那就是一个内建的 `toString` 方法,但是它在哪里呢?`obj` 是空的!

...然而简短的表达式 `obj = {}` 和 `obj = new Object()` 是一个意思,其中 `Object` 是一个内置的对象构造函数。并且这个方法有一个 `Object.prototype` 属性,这个属性引用了一个庞大的对象,这个庞大的对象有 `toString` 方法和其他的一些方法
……然而简短的表达式 `obj = {}` 和 `obj = new Object()` 是一个意思,其中 `Object` 就是一个内建的对象构造函数,其自身的 `prototype` 指向一个带有 `toString` 和其他方法的一个巨大的对象

就像这样(所有的这些都是内置的)
就像这样:

![](object-prototype.svg)

当 `new Object()` 被调用来创建一个对象(或者一个字面量对象 `{...}` 被创建),按照前面章节的讨论规则,这个对象的 `[[Prototype]]` 属性被设置为 `Object.prototype`:
当 `new Object()` 被调用(或一个字面量对象 `{...}` 被创建),按照前面章节中我们学习过的规则,这个对象的 `[[Prototype]]` 属性被设置为 `Object.prototype`:

![](object-prototype-1.svg)

之后当 `obj.toString()` 被调用时,这个方法是在 `Object.prototype` 中被取到的
所以,之后当 `obj.toString()` 被调用时,这个方法是从 `Object.prototype` 中获取的

我们可以这样验证它:

Expand All @@ -36,21 +36,21 @@ alert(obj.__proto__ === Object.prototype); // true
// obj.toString === obj.__proto__.toString == Object.prototype.toString
```

请注意在 `Object.prototype` 上没有额外的 `[[Prototype]]` 属性
请注意在 `Object.prototype` 上方的链中没有更多的 `[[Prototype]]`:

```js run
alert(Object.prototype.__proto__); // null
```

## 其他内置原型
## 其他内建原型

像 `Array`、`Date`、`Function` 和其他的内置对象都在原型对象上挂载方法
其他内建对象,像 `Array`、`Date`、`Function` 及其他,都在 prototype 上挂载了方法

例如,当我们创建一个数组 `[1, 2, 3]`,内部使用默认的 `new Array()` 构造函数。因此这个数组数据被写进了这个新的数组对象,并且 `Array.prototype` 成为这个数组对象的原型且为数组对象提供数组的操作方法。这样内存的存储效率是很高的。
例如,当我们创建一个数组 `[1, 2, 3]`,在内部会默认使用 `new Array()` 构造函数。因此 `Array.prototype` 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。

按照规范,所有的内置原型顶端都是 `Object.prototype`。有些时候人们说“一切都从对象上继承而来”。
按照规范,所有的内建原型顶端都是 `Object.prototype`。这就是为什么有人说“一切都从对象继承而来”。

下面是完整的示意图(3 个内置对象):
下面是完整的示意图(3 个内建对象):

![](native-prototypes-classes.svg)

Expand All @@ -59,34 +59,34 @@ alert(Object.prototype.__proto__); // null
```js run
let arr = [1, 2, 3];

// it inherits from Array.prototype?
// 它继承自 Array.prototype
alert( arr.__proto__ === Array.prototype ); // true

// then from Object.prototype?
// 接下来继承自 Object.prototype
alert( arr.__proto__.__proto__ === Object.prototype ); // true

// and null on the top.
// 原型链的顶端为 null
alert( arr.__proto__.__proto__.__proto__ ); // null
```

一些方法可能在原型上发生重叠,例如,`Array.prototype` 有自己的 `toString` 方法来列举出来数组的所有元素并用逗号分隔每一个元素。
一些方法在原型上可能会发生重叠,例如,`Array.prototype` 有自己的 `toString` 方法来列举出来数组的所有元素并用逗号分隔每一个元素。

```js run
let arr = [1, 2, 3]
alert(arr); // 1,2,3 <-- the result of Array.prototype.toString
alert(arr); // 1,2,3 <-- Array.prototype.toString 的结果
```

正如我们之前看到的那样,`Object.prototype` 也有 `toString` 方法,但是 `Array.prototype` 在原型链上是更近的,所以数组对象原型上的方法会被使用。
正如我们之前看到的那样,`Object.prototype` 也有 `toString` 方法,但是 `Array.prototype` 在原型链上更近,所以数组对象原型上的方法会被使用。


![](native-prototypes-array-tostring.svg)


像 Chrome 开发者控制台这样的浏览器内置工具也显示继承关系的(可能需要对内置对象使用 `console.dir`):
浏览器内的工具,像 Chrome 开发者控制台也会显示继承性(可能需要对内置对象使用 `console.dir`):

![](console_dir_array.png)

其他内置对象以同样的方式运行。即使是函数。它们是内置构造函数 `Function` 创建出来的对象,并且他们的方法:`call/apply` 和其他方法都来自 `Function.prototype`。函数也有自己的 `toString` 方法。
其他内建对象也以同样的方式运行。即使是函数 —— 它们是内建构造函数 `Function` 的对象,并且它们的方法(`call`/`apply` 及其他)都取自 `Function.prototype`。函数也有自己的 `toString` 方法。

```js run
function f() {}
Expand All @@ -99,17 +99,17 @@ alert(f.__proto__.__proto__ == Object.prototype); // true, inherit from objects

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

正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时的包装对象将会被内置的构造函数`String`、 `Number` `Boolean`创建,它们提供给我们操作字符串、数字和布尔值的方法然后藏匿起来。(译者注:这里的“隐匿起来”应该是指我们在打印这些值的时候看不到对象的方法)
正如我们记忆中的那样,它们并不是对象。但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造函数 `String`、`Number` `Boolean` 被创建。它们提供给我们操作字符串、数字和布尔值的方法然后消失。

这些对象对我们来说是被无形的创造出来的且大多数引擎优化了它们,而规范精准的描述了这种方式。这些对象的方法也驻留在它们的原型 `String.prototype`、`Number.prototype` 和 `Boolean.prototype`
这些对象对我们来说是无形地创建出来的。大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式。这些对象的方法也驻留在它们的 prototype 中,可以通过 `String.prototype`、`Number.prototype` 和 `Boolean.prototype` 进行获取

```warn header="值 `null` 和 `undefined` 没有对象包装"
特殊值 `null` 和 `undefined` 要被区分看待。它们没有对象包装,所以它们没有自己的方法和属性。并且它们没有相应的原型
```warn header="值 `null` 和 `undefined` 没有对象包装器"
特殊值 `null` 和 `undefined` 比较特殊。它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型
```

## 更改原生原型 [#原生-原型-更改]
## 更改原生原型 [#native-prototype-change]

原生的原型是可以被修改的。例如,我们在 `String.prototype` 中添加一个方法,这个方法对所有的字符串都是可用的
原生的原型是可以被修改的。例如,我们向 `String.prototype` 中添加一个方法,这个方法将对所有的字符串都是可用的

```js run
String.prototype.show = function() {
Expand All @@ -119,62 +119,75 @@ String.prototype.show = function() {
"BOOM!".show(); // BOOM!
```

在开发的过程中我们可能会想在内置对象上创建一个我们想要的方法。把他们添加到原生对象的原型中可能看起来不错,但那样通常来说是个坏主意
在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中。但这通常是一个很不好的想法

原型是全局的,所以很容易造成冲突。如果有两个代码段都添加了 `String.prototype.show` 方法,那么其中一个将覆盖另外一个。
```warn
原型是全局的,所以很容易造成冲突。如果有两个库都添加了 `String.prototype.show` 方法,那么其中的一个方法将被另一个覆盖。

在现代编程中,只有一种情况下修改原生原型是被允许的。那就是在做 polyfills (译者注:兼容)的时候。换句话说,如果有一个 JavaScript 规范还没有被我们的 JavaScript 引擎支持(或者我们希望被支持的那些规范),那么需要手动实现这些规范并且把这些手动实现填充到内置对象的原型上。
所以,通常来说,修改原生原型被认为是一个很不好的想法。
```

**在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling。**

Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型。

例如:

```js run
if (!String.prototype.repeat) { //假设没有这个方法
//把它添加到原型上
if (!String.prototype.repeat) { // 如果这儿没有这个方法
// 那就在 prototype 中添加它

String.prototype.repeat = function(n) {
//重复字符串 n 次
// 重复传入的字符串 n 次

//实际上代码是比这个更复杂的,
//当 "n" 的值为负数的时候抛出异常
//完整的算法在规范中
// 实际上,实现代码比这个要复杂一些(完整的方法可以在规范中找到)
// 但即使是不够完美的 polyfill 也常常被认为是足够好的
return new Array(n + 1).join(this);
};
}

alert( "La".repeat(3) ); // LaLaLa
```


## 从原型中借用

在 <info:call-apply-decorators#method-borrowing> 章节中我们讨论的方法借用:
在 <info:call-apply-decorators#method-borrowing> 一章中,我们讨论了方法借用。

那是指我们从一个对象获取一个方法,并将其复制到另一个对象。

一些原生原型的方法通常会被借用。

例如,如果我们要创建类数组对象,则可能需要向其中复制一些 `Array` 方法。

例如:

```js run
function showArgs() {
let obj = {
0: "Hello",
1: "world!",
length: 2,
};

*!*
// 从数组借用 join 方法并在 arguments 的上下文中调用
alert( [].join.call(arguments, " - ") );
obj.join = Array.prototype.join;
*/!*
}

showArgs("John", "Pete", "Alice"); // John - Pete - Alice
alert( obj.join(',') ); // Hello,world!
```

因为 `join` 方法在 `Array.prototype` 对象上,我们可以直接调用它并且重写上面的代码:
上面这段代码有效,是因为内建的方法 `join` 的内部算法只关心正确的索引和 `length` 属性。它不会检查这个对象是否是真正的数组。许多内建方法就是这样。

```js
function showArgs() {
*!*
alert( Array.prototype.join.call(arguments, " - ") );
*/!*
}
```
另一种方式是通过将 `obj.__proto__` 设置为 `Array.prototype`,这样 `Array` 中的所有方法都自动地可以在 `obj` 中使用了。

但是如果 `obj` 已经从另一个对象进行了继承,那么这种方法就不可行了。请记住,我们一次只能继承一个对象。

这样是更有效率的,因为它避免了一个额外数组对象 `[]` 的创建。另一方面,这样做,需要更长的时间来编写
方法借用很灵活,它允许在需要时混合来自不同对象的方法

## 总结

- 所有的内置对象都遵循一样的模式
- 方法都存储在原型对象上(`Array.prototype`、`Object.prototype`、`Date.prototype` 等)。
- 所有的内建对象都遵循相同的模式(pattern)
- 方法都存储在 prototype 中(`Array.prototype`、`Object.prototype`、`Date.prototype` 等)。
- 对象本身只存储数据(数组元素、对象属性、日期)。
- 基本数据类型同样在包装对象的原型上存储方法:`Number.prototype`、`String.prototype` 和 `Boolean.prototype`。只有 `undefined` 和 `null` 没有包装对象
- 内置对象的原型可以被修改或者被新的方法填充。但是这样做是不被推荐的。只有当添加一个还没有被 JavaScript 引擎支持的新方法的时候才可能允许这样做
- 原始数据类型也将方法存储在包装器对象的 prototype 中:`Number.prototype`、`String.prototype` 和 `Boolean.prototype`。只有 `undefined` 和 `null` 没有包装器对象
- 内建原型可以被修改或被用新的方法填充。但是不建议更改它们。唯一允许的情况可能是,当我们添加一个还没有被 JavaScript 引擎支持,但已经被加入 JavaScript 规范的新标准时,才可能允许这样做