一、JavaScript 在处理函数时提供了非凡的灵活性。它们可以被传递,用作对象,现在我们将看到如何在它们之间转发(forward)调用并装饰(decorate)它们。

call

func.call()

一、有一个特殊的内置函数方法func.call(context, …args),它允许调用一个显式设置this的函数。
二、语法如下:

  1. func.call(context, arg1, arg2, ...)

1、它运行func,提供的第一个参数作为this,后面的作为参数(arguments)。
2、简单地说,这两个调用几乎相同:

func(1, 2, 3);
func.call(obj, 1, 2, 3)

(1)它们调用的都是func,参数是1、2和3。唯一的区别是func.call还会将this设置为obj。

| 【示例】我们在不同对象的上下文中调用sayHi: sayHi.call(user)运行sayHi并提供了this.user,然后下一行设置this=admin```javascript function sayHi() { alert(this.name); }

let user = { name: “John” }; let admin = { name: “Admin” };

// 使用 call 将不同的对象传递为 “this” sayHi.call( user ); // John sayHi.call( admin ); // Admin

 |
| --- |

| 【示例】用带有给定上下文和phrase的call调用say```javascript
function say(phrase) {
  alert(this.name + ': ' + phrase);
}

let user = { name: "John" };

// user 成为 this,"Hello" 成为第一个参数
say.call( user, "Hello" ); // John: Hello

| | —- |

apply

一、内建方法func.apply的语法是:

func.apply(context, args)
  • context:上下文。
    • this=context
    • 如果context为空,则使用全局对象代替
  • args:参数列表。
    • 为类数组对象 | 【示例】```javascript // 严格模式 ‘use strict’;

function sum() { console.log(this); }

sum.apply(); // undefined sum.apply(null); // null sum.apply(undefind); // undefined sum.apply(1) // 1 sum.apply(‘hello’); // ‘hello’

```javascript
// 非严格模式
function sum() {
    console.log(this);
}

sum.apply(); // Window对象
sum.apply(null); // Window对象
sum.apply(undefind); // Window对象
sum.apply(1); // Number对象,Number{1}
sum.apply('hello'); // String对象,String{'hello'}

| | —- |

二、apply可以将一个数组转换为一个参数列表([p1, p2, p3]转换为p1, p2, p3)

| 【示例】```javascript var arr = [1, 2, 3, 4]

// Math.max只能传数字,可以使用apply将数组转为一个一个参数传入,相当于是Math.max(1, 2, 3, 4) console.log(Math.max.apply(null, arr)); // 4

 |
| --- |

<a name="ukPog"></a>
# call、apply、bind
<a name="PYMFr"></a>
## call、apply
一、call和apply之间唯一的语法区别是,call期望一个参数列表,而apply期望一个包含这些参数的类数组对象。<br />1、因此,这两个调用几乎是等效的:
```javascript
func.call(context, ...args); // 使用 spread 语法将数组作为列表传递
func.apply(context, args);   // 与使用 call 相同

2、这里只有很小的区别:

  • Spread 语法…允许将可迭代对象args作为列表传递给call。
  • apply仅接受类数组对象args。

二、使用场景
1、当我们期望可迭代对象时,使用call,当我们期望类数组对象时,使用apply。
2、对于即可迭代又是类数组的对象,例如一个真正的数组,我们使用call或apply均可,但是apply可能会更快,因为大多数 JavaScript 引擎在内部对其进行了优化。
三、将所有参数连同上下文一起传递给另一个函数被称为“呼叫转移(call forwarding)”。
1、这是它的最简形式:

let wrapper = function() {
  return func.apply(this, arguments);
};

(1)当外部代码调用这种包装器wrapper时,它与原始函数func的调用是无法区分的。

call、apply、bind

一、用途:手动改变this的指向
二、区别

  • apply和call会使当前函数立即执行,bind会返回一个函数,后续需要时再调用
  • call是apply的语法糖,只有传的参数不同,call中药传多个任意参数,apply只可以直接传数组或类数组。
  • bind是为函数绑定一个this上下文

三、规则

fn.apply(上下文环境,执行所需数组)
fn.call(上下文环境, 执行所需单个参数)
fn.bind(上下文环境)
  • 如果上下文的值为null,则使用全局对象代替,相当于没传上下文还用以前的

示例

透明缓存

| 【☆示例】期望:有一个CPU重负载的函数slow(x),但它的结果时稳定的。换句话说,对于相同的x,它总是返回相同的结果。如果经常调用该函数,我们可能希望将结果缓存(记住)下来,以避免在重新计算上话费额外的时间。
实现:不是将这个功能添加到slow()中,而是创建一个包装器(wrapper)函数,该函数增加了缓存功能。
代码``javascript function slow(x) { // 这里可能会有重负载的 CPU 密集型工作 alert(Called with ${x}`); return x; }

function cachingDecorator(func) { let cache = new Map();

return function(x) { if (cache.has(x)) { // 如果缓存中有对应的结果 return cache.get(x); // 从缓存中读取结果 }

let result = func(x);  // 否则就调用 func

cache.set(x, result);  // 然后将结果缓存(记住)下来
return result;

}; }

slow = cachingDecorator(slow);

alert( slow(1) ); // slow(1) 被缓存下来了 alert( “Again: “ + slow(1) ); // 一样的

alert( slow(2) ); // slow(2) 被缓存下来了 alert( “Again: “ + slow(2) ); // 和前面一行结果相同

**解释**:1、cachingDecorator是一个装饰器(decorator):一个特殊的函数,它接受另一个函数并改变它的行为。<br />2、其思想是,我们可以为任何函数调用cachingDecorator,它将返回缓存包装器。<br />3、通过将缓存与主函数代码分开,我们还可以使主函数代码变得简单。<br />4、cachingDecorator(func)的结果是一个“包装器”:function(x)将func(x)的调用“包装”到缓存逻辑中:<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/355497/1619763211041-39cfb740-ab84-4835-8522-bf5dcc618f42.png#clientId=u48d3b3c1-4e9a-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=256&id=ued461c21&margin=%5Bobject%20Object%5D&name=image.png&originHeight=256&originWidth=463&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24864&status=done&style=none&taskId=u6bffb4a2-0842-43f2-8b39-4205b144313&title=&width=463)<br />5、从外部代码来看,包装的slow函数执行的仍然是与之前相同的操作。它只是在其行为上添加了缓存个功能。<br />**使用分离的cachingDecorator而不是改变slow本身的代码的好处**:<br />1、cachingDecorator是可重用的。我们可以将它应用于另一个函数。<br />2、缓存逻辑是独立的,它没有增加slow本身的复杂性(如果有的话)。<br />3、如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)。 |
| --- |

<a name="RQlAV"></a>
## 使用 “func.call” 设定上下文
一、上面提到的缓存装饰器不适用于对象方法。

| 【☆示例】在下面的代码中,worker.slow()在装饰后停止工作:```javascript
// 我们将对 worker.slow 的结果进行缓存
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    // 可怕的 CPU 过载任务
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

// 和之前例子中的代码相同
function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func(x); // (**)
    cache.set(x, result);
    return result;
  };
}

alert( worker.slow(1) ); // 原始方法有效

worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存

alert( worker.slow(2) ); // Error: Cannot read property 'someMethod' of undefined

1、错误发生在试图访问this.someMethod并失败了的()行中。
2、原因是包装器将原始函数调用为(*
)行中的func(x)。并且,当这样调用时,函数将得到this = undefined。 | | —- |

| 【示例】如果尝试运行下面这段代码,我们会观察到类似的问题:```javascript let func = worker.slow; func(2);

1、因此,包装器将调用传递给原始方法,但没有上下文this。因此,发生了错误。 |
| --- |

二、func.call()可以解决这个问题。
<a name="CHXN4"></a>
### 解决方案
| 【☆示例】我们在不同对象的上下文中调用sayHi:sayHi.call(user)运行sayHi并提供了this=user,然后下一行设置this=admin:<br />1、在这里我们用带有给定上下文和 phrase 的call调用say:<br />2、在我们的例子中,我们可以在包装器中使用call将上下文传递给原始函数:```javascript
let worker = {
  someMethod() {
    return 1;
  },

  slow(x) {
    alert("Called with " + x);
    return x * this.someMethod(); // (*)
  }
};

function cachingDecorator(func) {
  let cache = new Map();
  return function(x) {
    if (cache.has(x)) {
      return cache.get(x);
    }
    let result = func.call(this, x); // 现在 "this" 被正确地传递了
    cache.set(x, result);
    return result;
  };
}

worker.slow = cachingDecorator(worker.slow); // 现在对其进行缓存

alert( worker.slow(2) ); // 工作正常
alert( worker.slow(2) ); // 工作正常,没有调用原始函数(使用的缓存)

3、看看this是如何被传递的:
(1)在经过装饰之后,worker.slow现在是包装器function (x) { … }。
(2)因此,当worker.slow(2)执行时,包装器将2作为参数,并且this=worker(它是点符号.之前的对象)。
(3)在包装器内部,假设结果尚未缓存,func.call(this, x)将当前的this(=worker)和当前的参数(=2)传递给原始方法。 | | —- |

传递多个参数

一、现在让我们把cachingDecorator写得更加通用。到现在为止,它只能用于单参数函数。
二、现在如何缓存多参数worker.slow方法呢?

let worker = {
  slow(min, max) {
    return min + max; // scary CPU-hogger is assumed
  }
};

// 应该记住相同参数的调用
worker.slow = cachingDecorator(worker.slow);

1、之前,对于单个参数x,我们可以只使用cache.set(x, result)来保存结果,并使用cache.get(x)来检索并获取结果。但是现在,我们需要记住参数组合(min,max)的结果。原生的Map仅将单个值作为键(key)。
三、这儿有许多解决方案可以实现:

  1. 实现一个新的(或使用第三方的)类似 map 的更通用并且允许多个键的数据结构。
  2. 使用嵌套 map:cache.set(min)将是一个存储(键值)对(max, result)的Map。所以我们可以使用cache.get(min).get(max)来获取result。
  3. 将两个值合并为一个。为了灵活性,我们可以允许为装饰器提供一个“哈希函数”,该函数知道如何将多个值合并为一个值。

对于许多实际应用,第三种方式就足够了,所以我们就用这个吧。
四、当然,我们需要传入的不仅是x,还需要传入func.call的所有参数。让我们回想一下,在function()中我们可以得到一个包含所有参数的伪数组(pseudo-array)arguments,那么func.call(this, x)应该被替换为func.call(this, …arguments)。
五、这是一个更强大的cachingDecorator:

let worker = {
  slow(min, max) {
    alert(`Called with ${min},${max}`);
    return min + max;
  }
};

function cachingDecorator(func, hash) {
  let cache = new Map();
  return function() {
    let key = hash(arguments); // (*)
    if (cache.has(key)) {
      return cache.get(key);
    }

    let result = func.call(this, ...arguments); // (**)

    cache.set(key, result);
    return result;
  };
}

function hash(args) {
  return args[0] + ',' + args[1];
}

worker.slow = cachingDecorator(worker.slow, hash);

alert( worker.slow(3, 5) ); // works
alert( "Again " + worker.slow(3, 5) ); // same (cached)

1、现在这个包装器可以处理任意数量的参数了(尽管哈希函数还需要被进行调整以允许任意数量的参数。一种有趣的处理方法将在下面讲到)。
2、这里有两个变化:

  • 在(*)行中它调用hash来从arguments创建一个单独的键。这里我们使用一个简单的“连接”函数,将参数(3, 5)转换为键”3,5”。更复杂的情况可能需要其他哈希函数。
  • 然后(**)行使用func.call(this, …arguments)将包装器获得的上下文和所有参数(不仅仅是第一个参数)传递给原始函数。

    func.apply

    一、我们可以使用func.apply(this, arguments)代替func.call(this, …arguments)。
    十、示例
【示例】语句var arr=[a,b,c,d];执行后,数组arr中每项都是一个整数,下面得到其中最大整数语句正确的是哪几项?
A. Math.max(arr)
B. Math.max(arr[0], arr[1], arr[2], arr[3])
C. Math.max.call(Math, arr[0], arr[1], arr[2], arr[3])
D. Math.max.apply(Math,arr)
答案:BCD
解析

借用一种方法

一、现在,让我们对哈希函数再做一个较小的改进:

function hash(args) {
  return args[0] + ',' + args[1];
}

二、截至目前,它仅适用于两个参数。如果它可以适用于任何数量的args就更好了。
三、自然的解决方案是使用arr.join方法:

function hash(args) {
  return args.join();
}

四、不幸的是,这不行。因为我们正在调用hash(arguments),arguments对象既是可迭代对象又是类数组对象,但它并不是真正的数组。
五、所以在它上面调用join会失败,我们可以在下面看到:

function hash() {
  alert( arguments.join() ); // Error: arguments.join is not a function
}

hash(1, 2);

六、不过,有一种简单的方法可以使用数组的 join 方法:

function hash() {
  alert( [].join.call(arguments) ); // 1,2
}

hash(1, 2);

1、这个技巧被称为方法借用(method borrowing)。
七、我们从常规数组[].join中获取(借用)join 方法,并使用[].join.call在arguments的上下文中运行它。
八、它为什么有效?
九、那是因为原生方法arr.join(glue)的内部算法非常简单。
十、从规范中几乎“按原样”解释如下:

  1. 让glue成为第一个参数,如果没有参数,则使用逗号”,”。
  2. 让result为空字符串。
  3. 将this[0]附加到result。
  4. 附加glue和this[1]。
  5. 附加glue和this[2]。
  6. ……以此类推,直到this.length项目被粘在一起。
  7. 返回result。

十一、因此,从技术上讲,它需要this并将this[0],this[1]……等 join 在一起。它的编写方式是故意允许任何类数组的this的(不是巧合,很多方法都遵循这种做法)。这就是为什么它也可以和this=arguments一起使用。

装饰器和函数属性

一、通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西。如果原始函数有属性,例如func.calledCount或其他,则装饰后的函数将不再提供这些属性。因为这是装饰器。因此,如果有人使用它们,那么就需要小心。
二、例如,在上面的示例中,如果slow函数具有任何属性,而cachingDecorator(slow)则是一个没有这些属性的包装器。
三、一些包装器可能会提供自己的属性。例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(expose)这些信息。
四、存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的Proxy对象来包装函数。我们将在后面的Proxy 和 Reflect中学习它。

总结

一、装饰器是一个围绕改变函数行为的包装器。主要工作仍由该函数来完成。
二、装饰器可以被看作是可以添加到函数的 “features” 或 “aspects”。我们可以添加一个或添加多个。而这一切都无需更改其代码!
三、为了实现cachingDecorator,我们研究了以下方法:

四、通用的呼叫转移(call forwarding)通常是使用apply完成的:
let wrapper = function() {
return original.apply(this, arguments);};
五、我们也可以看到一个方法借用(method borrowing)的例子,就是我们从一个对象中获取一个方法,并在另一个对象的上下文中“调用”它。采用数组方法并将它们应用于参数arguments是很常见的。另一种方法是使用 Rest 参数对象,该对象是一个真正的数组。
六、在 JavaScript 领域里有很多装饰器(decorators)。通过解决本章的任务,来检查你掌握它们的程度吧。