装饰器模式和转发,call/apply
概述
JavaScript
在处理函数时提供了非凡的灵活性- 它们可以被传递,用作对象,也可以在它们之间 转发(
forward
) 调用并 装饰(decorate
) 它们
透明缓存
- 假设我们有一个
CPU
重负载的函数slow(x)
,但它的结果是稳定的- 换句话说,对于相同的
x
,它总是返回相同的结果 - 如果经常调用该函数,我们可能希望将结果缓存(记住)下来,以避免在重新计算上花费额外的时间
- 但是我们不是将这个功能添加到
slow()
中,而是创建一个包装器(wrapper
)函数,该函数增加了缓存功能
- 换句话说,对于相同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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) ); // 返回缓存中的 slow(1) 的结果 alert( slow(2) ); // slow(2) 被缓存下来了,并返回结果 alert( "Again: " + slow(2) ); // 返回缓存中的 slow(2) 的结果 |
- 上面的代码中,
cachingDecorator
是一个 装饰器(decorator
):- 一个特殊的函数,它接受另一个函数并改变它的行为
- 其思想是,我们可以为任何函数调用
cachingDecorator
,它将返回缓存包装器
- 总而言之,使用分离的
cachingDecorator
而不是改变slow
本身的代码有几个好处:cachingDecorator
是可重用的。我们可以将它应用于另一个函数- 缓存逻辑是独立的,它没有增加
slow
本身的复杂性(如果有的话) - 如果需要,我们可以组合多个装饰器(其他装饰器将遵循同样的逻辑)
使用 “func.call
” 设定上下文
- 面提到的缓存装饰器不适用于对象方法
- 例如,在下面的代码中,
worker.slow()
在装饰后停止工作:- 错误发生在试图访问
this.someMethod
并失败了的(*)
行中 - 原因是包装器将原始函数调用为
(**)
行中的func(x)
。并且,当这样调用时,函数将得到this = undefined
- 因此,包装器将调用传递给原始方法,但没有上下文
this
。因此,发生了错误
- 错误发生在试图访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 我们将对 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 |
- 有一个特殊的内建函数方法
func.call(context, …args)
,它允许调用一个显式设置this
的函数- 它运行
func
,提供的第一个参数作为this
,后面的作为参数(arguments
)
- 它运行
1 |
func.call(context, arg1, arg2, ...) |
1 2 3 4 |
// 几乎相同 func(1, 2, 3); func.call(obj, 1, 2, 3) |
- 例如,在下面的代码中,我们在不同对象的上下文中调用
sayHi
:sayHi.call(user)
运行sayHi
并提供了this=user
,然后下一行设置this=admin
:
1 2 3 4 5 6 7 8 9 10 |
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
:
1 2 3 4 5 6 7 8 |
function say(phrase) { alert(this.name + ': ' + phrase); } let user = { name: "John" }; // user 成为 this,"Hello" 成为第一个参数 say.call( user, "Hello" ); // John: Hello |
- 我们的例子中,我们可以在包装器中使用
call
将上下文传递给原始函数:- 现在一切都正常工作了
- 在经过装饰之后,
worker.slow
现在是包装器function (x) { ... }
- 因此,当
worker.slow(2)
执行时,包装器将2
作为参数,并且this=worker
(它是点符号.
之前的对象) - 在包装器内部,假设结果尚未缓存,
func.call(this, x)
将当前的this
(=worker
)和当前的参数(=2
)传递给原始方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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) ); // 工作正常,没有调用原始函数(使用的缓存) |
传递多个参数
- 现在让我们把
cachingDecorator
写得更加通用 - 如何缓存多参数
worker.slow
方法呢- 之前,对于单个参数
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 将两个值合并为一个。为了灵活性,我们可以允许为装饰器提供一个“哈希函数”,该函数知道如何将多个值合并为一个值
- 之前,对于单个参数
1 2 3 4 5 6 7 8 |
let worker = { slow(min, max) { return min + max; // scary CPU-hogger is assumed } }; // 应该记住相同参数的调用 worker.slow = cachingDecorator(worker.slow); |
- 第三种方式
- 当然,我们需要传入的不仅是
x
,还需要传入func.call
的所有参数 - 在
function()
中我们可以得到一个包含所有参数的伪数组(pseudo-array)arguments
,那么func.call(this, x)
应该被替换为func.call(this, ...arguments)
- 当然,我们需要传入的不仅是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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) |
- 现在这个包装器可以处理任意数量的参数了(尽管哈希函数还需要被进行调整以允许任意数量的参数)
- 在
(*)
行中它调用hash
来从arguments
创建一个单独的键。这里我们使用一个简单的“连接”函数,将参数(3, 5)
转换为键"3,5"
。更复杂的情况可能需要其他哈希函数 - 然后
(**)
行使用func.call(this, ...arguments)
将包装器获得的上下文和所有参数(不仅仅是第一个参数)传递给原始函数
- 在
func.apply
- 可以使用
func.apply(this, arguments)
代替func.call(this, ...arguments)
- 语法
- 它运行
func
设置this=context
,并使用类数组对象args
作为参数列表(arguments
)
- 它运行
1 |
func.apply(context, args) |
call
和apply
之间唯一的语法区别是,call
期望一个参数列表,而apply
期望一个包含这些参数的类数组对象- 因此,这两个调用几乎是等效的:
- 它们使用给定的上下文和参数执行相同的
func
调用
1 2 |
func.call(context, ...args); func.apply(context, args); |
- 只有一个关于
args
的细微的差别:Spread
语法...
允许将 可迭代对象args
作为列表传递给call
apply
只接受 类数组args
- 对于即可迭代又是类数组的对象,例如一个真正的数组,我们使用
call
或apply
均可,但是apply
可能会更快,因为大多数JavaScript
引擎在内部对其进行了优化
呼叫转移
- 将所有参数连同上下文一起传递给另一个函数被称为“呼叫转移(
call forwarding
)”
1 2 3 |
let wrapper = function() { return func.apply(this, arguments); }; |
修改上面的hash
函数
- 目前,它仅适用于两个参数
1 2 3 |
function hash(args) { return args[0] + ',' + args[1]; } |
- 如果它可以适用于任何数量的
args
就更好了- 不幸的是,这不行。因为我们正在调用
hash(arguments)
,arguments
对象既是可迭代对象又是类数组对象,但它并不是真正的数组 - 所以在它上面调用
join
会失败,我们可以在下面看到:
- 不幸的是,这不行。因为我们正在调用
1 2 3 |
function hash(args) { return args.join(); } |
1 2 3 4 5 |
function hash() { alert( arguments.join() ); // Error: arguments.join is not a function } hash(1, 2); |
- 不过,有一种简单的方法可以使用数组的
join
方法:- 这个技巧被称为 方法借用(
method borrowing
) - 我们从常规数组
[].join
中获取(借用)join 方法,并使用[].join.call
在arguments
的上下文中运行它
- 这个技巧被称为 方法借用(
1 2 3 4 5 |
function hash() { alert( [].join.call(arguments) ); // 1,2 } hash(1, 2); |
装饰器和函数属性
- 通常,用装饰的函数替换一个函数或一个方法是安全的,除了一件小东西
- 如果原始函数有属性,例如
func.calledCount
或其他,则装饰后的函数将不再提供这些属性 - 例如,在上面的示例中,如果
slow
函数具有任何属性,而cachingDecorator(slow)
则是一个没有这些属性的包装器
- 如果原始函数有属性,例如
- 一些包装器可能会提供自己的属性
- 例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(
expose
)这些信息
- 例如,装饰器会计算一个函数被调用了多少次以及花费了多少时间,并通过包装器属性公开(
- 存在一种创建装饰器的方法,该装饰器可保留对函数属性的访问权限,但这需要使用特殊的
Proxy
对象来包装函数
函数绑定
概述
- 当将对象方法作为回调进行传递,例如传递给
setTimeout
,这儿会存在一个常见的问题:“丢失this
”
丢失 this
- 我们已经看到了丢失
this
的例子- 一旦方法被传递到与对象分开的某个地方 ——
this
就丢失
- 一旦方法被传递到与对象分开的某个地方 ——
- 下面是使用
setTimeout
时this
是如何丢失的:- 正如我们所看到的,输出没有像
this.firstName
那样显示 “John
”,而显示了undefined
! - 这是因为
setTimeout
获取到了函数user.sayHi
,但它和对象分离开了
- 正如我们所看到的,输出没有像
1 2 3 4 5 6 7 8 |
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Hello, undefined! |
1 2 3 4 |
// 相当于 let f = user.sayHi; setTimeout(f, 1000); // 丢失了 user 上下文 |
- 浏览器中的
setTimeout
方法有些特殊:它为函数调用设定了this=window
- 对于
Node.js
,this
则会变为计时器(timer
)对象 - 所以对于
this.firstName
,它其实试图获取的是window.firstName
,这个变量并不存在 - 在其他类似的情况下,通常
this
会变为undefined
- 对于
- 解决这个问题的方法1:包装器
- 现在它可以正常工作了,因为它从外部词法环境中获取到了
user
,就可以正常地调用方法了
- 现在它可以正常工作了,因为它从外部词法环境中获取到了
1 2 3 4 5 6 7 8 9 10 |
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(function() { user.sayHi(); // Hello, John! }, 1000); |
1 |
setTimeout(() => user.sayHi(), 1000); // Hello, John! |
- 解决这个问题的方法2:
bind
bind
- 它可以绑定
this
func.bind(context)
的结果是一个特殊的类似于函数的“外来对象(exotic object
)- 它可以像函数一样被调用,并且透明地(
transparently
)将调用传递给func
并设定this=context
- 换句话说,
boundFunc
调用就像绑定了this
的func
- 这里的
func.bind(user)
作为func
的“绑定的(bound)变体”,绑定了this=user
1 2 |
// 稍后将会有更复杂的语法 let boundFunc = func.bind(context); |
1 2 3 4 5 6 7 8 9 10 |
let user = { firstName: "John" }; function func() { alert(this.firstName); } let funcUser = func.bind(user); funcUser(); // John |
- 所有的参数(
arguments
)都被“原样”传递给了初始的func
,例如:- 在
(*)
行,我们取了方法user.sayHi
并将其绑定到user
</li> <li>
sayHi
是一个“绑定后(bound
)”的方法,它可以被单独调用,也可以被传递给setTimeout
—— 都没关系,函数上下文都会是正确的
- 在
1 2 3 4 5 6 7 8 9 10 11 12 |
let user = { firstName: "John" }; function func(phrase) { alert(phrase + ', ' + this.firstName); } // 将 this 绑定到 user let funcUser = func.bind(user); funcUser("Hello"); // Hello, John(参数 "Hello" 被传递,并且 this=user) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) // 可以在没有对象(译注:与对象分离)的情况下运行它 sayHi(); // Hello, John! setTimeout(sayHi, 1000); // Hello, John! // 即使 user 的值在不到 1 秒内发生了改变 // sayHi 还是会使用预先绑定(pre-bound)的值,该值是对旧的 user 对象的引用 user = { sayHi() { alert("Another user in setTimeout!"); } }; |
bindAll
- 如果一个对象有很多方法,并且我们都打算将它们都传递出去,那么我们可以在一个循环中完成所有方法的绑定:
1 2 3 4 5 |
for (let key in user) { if (typeof user[key] == 'function') { user[key] = user[key].bind(user); } } |
部分(应用)函数(Partial functions
)
- 我们不仅可以绑定
this
,还可以绑定参数(arguments
)- 虽然很少这么做,但有时它可以派上用场
- 它允许将上下文绑定为
this
,以及绑定函数的部分参数
1 |
let bound = func.bind(context, [arg1], [arg2], ...); |
- 例如,我们有一个乘法函数
mul(a, b)
:- 让我们使用
bind
在该函数基础上创建一个double
函数: - 对
mul.bind(null, 2)
的调用创建了一个新函数double
,它将调用传递到mul
,将null
绑定为上下文,并将2
绑定为第一个参数。并且,参数(arguments
)均被“原样”传递 - 注意,这里我们实际上没有用到
this
。但是bind
需要它,所以我们必须传入null
之类的东西
- 让我们使用
1 2 3 |
function mul(a, b) { return a * b; } |
1 2 3 4 5 6 7 8 9 |
function mul(a, b) { return a * b; } let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10 |
1 2 3 4 5 6 7 8 9 |
function mul(a, b) { return a * b; } let triple = mul.bind(null, 3); alert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15 |
- 为什么我们通常会创建一个部分应用函数?
- 好处是我们可以创建一个具有可读性高的名字(
double
,triple
)的独立函数。我们可以使用它,并且不必每次都提供一个参数,因为参数是被绑定了的 - 另一方面,当我们有一个非常灵活的函数,并希望有一个不那么灵活的变型时,部分应用函数会非常有用
- 例如,我们有一个函数
send(from, to, text)
。然后,在一个user
对象的内部,我们可能希望对它使用send
的部分应用函数变型:从当前 user 发送sendTo(to, text)
- 好处是我们可以创建一个具有可读性高的名字(
在没有上下文情况下的 partial
- 当我们想绑定一些参数(
arguments
),但是不想绑定上下文this
,应该怎么办- 例如,对于一个对象方法
- 原生的
bind
不允许这种情况。我们不可以省略上下文直接跳到参数(arguments
) - 幸运的是,仅绑定参数(
arguments
)的函数partial
比较容易实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function partial(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // 用法: let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // 添加一个带有绑定时间的 partial 方法 user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // 类似于这样的一些内容: // [10:00] John: Hello! |
partial(func[, arg1, arg2...])
调用的结果是一个包装器(*)
,它调用func
并具有以下内容:- 与它获得的函数具有相同的
this
(对于user.sayNow
调用来说,它是user
) - 然后给它
...argsBound
—— 来自于partial
调用的参数("10:00"
) - 然后给它
...args
—— 给包装器的参数("Hello"
)
- 与它获得的函数具有相同的
深入理解箭头函数
概述
- 箭头函数不仅仅是编写简洁代码的“捷径”。它还具有非常特殊且有用的特性
JavaScript
充满了我们需要编写在其他地方执行的小函数的情况arr.forEach(func)
——forEach
对每个数组元素都执行func
setTimeout(func)
——func
由内建调度器执行- 等等
JavaScript
的精髓在于创建一个函数并将其传递到某个地方- 在这样的函数中,我们通常不想离开当前上下文。这就是箭头函数的主战场啦
箭头函数没有 “this
”
- 箭头函数没有
this
。如果访问this
,则会从外部获取 - 例如,我们可以使用它在对象方法内部进行迭代:
- 这里
forEach
中使用了箭头函数,所以其中的this.title
其实和外部方法showList
的完全一样。那就是:group.title
- 这里
1 2 3 4 5 6 7 8 9 10 11 12 |
let group = { title: "Our Group", students: ["John", "Pete", "Alice"], showList() { this.students.forEach( student => alert(this.title + ': ' + student) ); } }; group.showList(); |
注意
- 不能对箭头函数进行
new
操作- 不具有
this
自然也就意味着另一个限制:箭头函数不能用作构造器(constructor)。不能用new
调用它们
- 不具有
和bind
相比
- 箭头函数
=>
和使用.bind(this)
调用的常规函数之间有细微的差别:.bind(this)
创建了一个该函数的“绑定版本”- 箭头函数
=>
没有创建任何绑定。箭头函数只是没有this
。this
的查找与常规变量的搜索方式完全相同:在外部词法环境中查找
箭头函数没有 “arguments
”
- 箭头函数也没有
arguments
变量 - 当我们需要使用当前的
this
和arguments
转发一个调用时,这对装饰器(decorators
)来说非常有用 - 例如,
defer(f, ms)
获得了一个函数,并返回一个包装器,该包装器将调用延迟ms
毫秒:
1 2 3 4 5 6 7 8 9 10 11 12 |
function defer(f, ms) { return function() { setTimeout(() => f.apply(this, arguments), ms); }; } function sayHi(who) { alert('Hello, ' + who); } let sayHiDeferred = defer(sayHi, 2000); sayHiDeferred("John"); // 2 秒后显示:Hello, John |
- 不用箭头函数的话,可以这么写:
- 在这里,我们必须创建额外的变量
args
和ctx
,以便setTimeout
内部的函数可以获取它们
- 在这里,我们必须创建额外的变量
1 2 3 4 5 6 7 8 |
function defer(f, ms) { return function(...args) { let ctx = this; setTimeout(function() { return f.apply(ctx, args); }, ms); }; } |
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!