Javascript装饰器与转发, call/apply
Javascript在处理函数上提供了非常的灵活性,它们可以被传递,作为对象使用,现在我们看看如何在他们之间如何转发调用和装饰。
透明式缓存
假设我们有个函数为slow(x)
,比较占用CPU资源,但结果是稳定的,换句话说,对相同的x返回总是相同。
如果该函数经常被调用,我们可能想到缓存(记住)针对不同x的结果,避免浪费额外的时间重新计算。
但我们不往slowly()
函数中增加功能,而是创建一个包装器,我们将看到,这样做有很多好处,这里是代码和解释说明:
function slow(x) {
// there can be a heavy CPU-intensive job here
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // if the result is in the map
return cache.get(x); // return it
}
let result = func(x); // otherwise call func
cache.set(x, result); // and cache (remember) the result
return result;
};
}
slow = cachingDecorator(slow);
alert( slow(1) ); // slow(1) is cached
alert( "Again: " + slow(1) ); // the same
alert( slow(2) ); // slow(2) is cached
alert( "Again: " + slow(2) ); // the same as the previous line
上面的代码中cachingDecorator
函数是一个装饰器,需要另一个函数作为参数,并改变其行为。
主要思想是,我们可以通过cachingDecorator
调用任何其他函数,并返回缓存包装器。非常好,因为我们有很多函数需要这样的功能,我们需要做的就是给它们应用cachingDecorator
函数。
为了从主函数中分离缓存功能,需要保持主函数更简化。现在让我们深入细节。
函数cachingDecorator(func)
是包装器:function(x)
是对func(x)
函数调用包装至缓存逻辑:
如我们所见,包装器没有改变返回func(x)
的结果,从外部代码看,包装器slow
还是仍然实现通用功能,仅给其增加了缓存方面功能。
总之,使用单独的cachingDecorator
函数而不是修改slow
函数代码有几个方面的优势:
- 函数
cachingDecorator
可以重用,可以应用至其他函数。 - 缓存逻辑是独立的,没有增加
slow
函数的复杂性。 - 如果需要,可以合并多个装饰器。
应用上下文使用func.call
上面提到的缓存装饰器不适合对象方法情况,举例,在下面代码中user.format()
在装饰后不工作:
// we'll make worker.slow caching
let worker = {
someMethod() {
return 1;
},
slow(x) {
// actually, there can be a scary CPU-heavy task here
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
// same code as before
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) ); // the original method works
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // Whoops! Error: Cannot read property 'someMethod' of undefined
错误发生在星号行,尝试运行this.someMethod
方法失败,你能看出来为什么吗?
原因是包装器调用原函数如func(x)
,在双**号行,当这样调用时,this=undefined
。
我们也可以观察到类似的症状,如果我们运行下面代码:
let func = worker.slow;
func(2);
所以,包装器传递原对象方法调用,但没有上下文this
,因此发送错误。
让我们修正该错误。
有一个特定的内置函数方法func.call(context, …args)
,允许调用一个函数并显示指定this
.
语法为:
func.call(context, arg1, arg2, ...)
运行func
,提供一个参数作为this
,后面的作为调用函数参数。为了简化,下面两个调用几乎相同:
func(1, 2, 3);
func.call(obj, 1, 2, 3)
他们都使用参数1,2,3
调用函数func
,唯一区别是func.call
同时设置this
为obj
。
下面示例代码中,在不同的对象上下文中调用sayHi
:
运行sayHi.call(user)
时,this=user
,下面一行设置this=admin
:
function sayHi() {
alert(this.name);
}
let user = { name: "John" };
let admin = { name: "Admin" };
// use call to pass different objects as "this"
sayHi.call( user ); // John
sayHi.call( admin ); // Admin
这里我们使用call调用say,使用给定的上下文和其他参数:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
let user = { name: "John" };
// user becomes this, and "Hello" becomes the first argument
say.call( user, "Hello" ); // John: Hello
上面示例的情况,我们能在包装器使用call并传递上下文给原函数:
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" is passed correctly now
cache.set(x, result);
return result;
};
}
worker.slow = cachingDecorator(worker.slow); // now make it caching
alert( worker.slow(2) ); // works
alert( worker.slow(2) ); // works, doesn't call the original (cached)
现在一切正常。为了更清除,我们更深入看看this如何传递的:
- 在
worker.slow
装饰后,现在包装器是function(x){...}
- 所以当执行
worker.slow(2)
时,包装器获得2作为参数,this=worker
(点前面的对象) - 在包装器内部,假设结果没有缓存,
func.call(this,x)
传递当前this=worker
和当前参数2至原方法
使用”func.apply”实现多参数
现在我们让cachingDecorator
函数更通用。到目前为止,仅支持单个参数。
如何让worker.slow
方法缓存多个参数?
let worker = {
slow(min, max) {
return min + max; // scary CPU-hogger is assumed
}
};
// should remember same-argument calls
worker.slow = cachingDecorator(worker.slow);
这里需解决两个任务。
首先如何使用两个参数min
和max
作为缓存map的键,前面是单个参数x,我们能直接通过cache.set(x,result)
保存结果,然后使用cache.get(x)
返回结果。但是我们需要使用复合参数记住结果(min,max)
.默认Map
只能使用一个值作为键。
有多个解决方法:
- 实现一个新的类map数据结构,其更通用且支持多个键。
- 使用嵌套的map:
cache.set(min)
作为map对象存储在对应的(max,result)
中。所以我们能这样获得结果:cache.get(min).get(max)
. - 链接两个值为一个,在我们特定的示例中,可以仅使用
min,max
字符串作为map
的键。为了更灵活,可以提供一个hashing函数,可以把多个值生成单个唯一值。
对大多数实践应用,使用第三种方式,这里我们也采纳。
第二个任务是怎么传递多个参数给func
,目前包装器function(x)
假定只有一个参数,func.call(this,x)
.
这里我们使用另一个内置方法func.apply
.语法为:
func.apply(context, args)
执行func
函数,设置this=context
并使用类似数组对象args
作为参数列表。
举例,下面两个调用几乎一致:
func(1, 2, 3);
func.apply(context, [1, 2, 3])
两者都使用参数1,2,3
运行函数func
,但apply
同时设置了this=context
.
举例,这里使this=user
,messageData
作为参数列表调用say
函数:
function say(time, phrase) {
alert(`[${time}] ${this.name}: ${phrase}`);
}
let user = { name: "John" };
let messageData = ['10:00', 'Hello']; // become time and phrase
// user becomes this, messageData is passed as a list of arguments (time, phrase)
say.apply(user, messageData); // [10:00] John: Hello (this=user)
apply与call之间仅有区别是,call
需参数列表,而apply
需要一个类数组对象。
我们已经知道spread操作符...
,我们可以传递一个数组或任何iterable作为参数列表,所以如果使用call,几乎能使用apply实现。
下面两次调用几乎等价:
let args = [1, 2, 3];
func.call(context, ...args); // pass an array as list with spread operator
func.apply(context, args); // is same as using apply
如果看仔细,两种使用有一点差别。
- spread操作符
...
允许传递iterable参数作为list给call - apply仅接受类数组参数
所以,两种互补,如期望iterable,使用call,当期望类数组,使用apply。
如果参数既为iterable也为类数组,如数组,那么从技术上两种都可以使用,但apply可能更快,因为它是单个操作,大多数Javascript引擎优化apply比call+spread
更好。
使用apply最重要的一个是传递调用给另一个函数,如:
let wrapper = function() {
return anotherFunction.apply(this, arguments);
};
这称为转发调用。包装器传递this上下文和参数给anotherFunction
并返回结果。
当一个外部代码调用这样包装器,和调用原始函数没有区别。
现在让我们继续打造更强大的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.apply(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
创建单一key。通过简单使用“joining”函数返回参数(3,5)
对应的key3,5
,更复杂的情况可能需要其他hashing函数。双星号(**)行使用
func.apply
,传递上下文和所有参数(无论有多少)给原始函数。
方法借用
现在让我们稍微对hash函数做些改进:
function hash(args) {
return args[0] + ',' + args[1];
}
到目前为止,只有两个参数,如果能适用任意数量参数更好。自然的解决方案是使用arr.join
方法:
function hash(args) {
return args.join();
}
不幸的是,不能正常工作,因为调用hash(arguments)
,arguments
对象是iterable和类数组,但不是数组。
所以调用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);
这个技巧称为“方法借用”。
我们从数组对象中借用方法join,如:[].join
,使用[].join.call
在arguments的上下文中运行。
为什么可以工作?
因为内置方法arr.join(glue)
的内部算法很简单,集合和规范一致:
- 设置
glue
作为第一个参数, 如果没有参数,那么为一个逗号","
. - 设置
result
为一个空字符串. - 追加
this[0]
到result
. - 追加
glue
到this[1]
. - 追加
glue
和this[2]
. - …一直做直到
this.length
个项目被加入. - 返回
result
.
所以,技术上设置this
,然后连接this[0],this[1]...
等在一起。这时有意写的一种方式,允许连接任何类数组类型(这不是一个巧合,许多方法都遵循这种方式)。
总结
装饰器是包装函数并修改其行为。主要功能仍有原函数完成。
装饰并代替函数或方法通常是安全的,但也有例外。如果原函数中有些属性,如func.calledCount
或其他,那么装饰后的函数不能提供,因为仅是一个包装器。所以使用他们需小心。一些装饰器提供了自己的属性。
装饰器可以看作原函数增加了一些“特性”或“方面”,我们可以增加一个或多个,但并没有改变原来代码。
为了实现cachingDecorator
,我们学习了方法:
func.call(context, arg1, arg2…)
– 调用 func 使用给订的上下文和参数。func.apply(context, args)
– 调用 func 传递 context 给 this ,类数组参数作为参数列表。
通常转发调用使用apply:
let wrapper = function() {
return original.apply(this, arguments);
}
我们也看到方法借用的示例,我们借用一个对象的方法,在另一个对象上下文中调用。通常借用数组方法应用在arguments上。替代方法是对真正数组使用rest参数对象。
还有很多其他的装饰器,可以尝试找到它们并应用解决本章任务。
本文参考链接:https://blog.csdn.net/neweastsun/article/details/74502871