引子
在研究恶魔金字塔(Pyramid of Doom)的过程中,注意到 async 有这样的示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
async.series([ function(callback){ // do some stuff ... callback(null, 'one'); }, function(callback){ // do some more stuff ... callback(null, 'two'); } ], // optional callback function(err, results){ // results is now equal to ['one', 'two'] }); |
令人寻味的是这个回调函数callback。它不是调用者写的,而是由series()通过高阶函数的方式注入的。怎么注入的?答案是 arguments。事实上,async 完美的运用了函数arguments 的动态特性。
说到这个 arguments呢,它其实不是个数组,toString 会报告它是一种 [object Arguments] 类型的数据。诚如 MDN 所述:
The
arguments
object is a local variable available within all functions…You can refer to a function’s arguments within the function by using the
arguments
object. This object contains an entry for each argument passed to the function, the first entry’s index starting at 0.
需要注意的是,Function对象只有在运行(call/apply)时,才会得到这个arguments对象,与你怎么声明这个函数签名没有关系。比如声明这么一个无参函数,实际调用你可以传入任意参数,arguments 能够感知:
1 2 3 4 5 |
function foolsGold(){ console.log(Array.prototype.slice.call(arguments)); } foolsGold('aha, I got it.'); // 运行结果: [ 'aha, I got it.' ] |
并且,这个arguments只在函数的作用域内可见。它的出现:在函数运行时,位于函数体内。
有了arguments,再配合 高阶函数,实际上我们可以做很多事情,比如对任意函数进行注入——更具体一点,回到恶魔金字塔(Pyramid of Doom)问题中,调用函数和被调函数约定:每个被调函数的最后一个参数,将一定会被调用方传入一个回调函数,被调函数将自觉遵守约定,在逻辑执行完成前,callback it! 然后,主调方将各个被调函数织入队列,统一在其 arguments 末尾注入一个 callback函数。
实验
假设callback长这样:
1 2 3 4 |
var callback = function(){ var args = Array.prototype.slice.call(arguments); console.log('callback was called, and arguments are: ' + args); }; |
被调函数callback时传入的arguments被转成一个Array,打印到console。
调用函数的注入高阶函数长这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var injectArgs = function(){ var args = Array.prototype.slice.call(arguments); if(args.length < 2) throw new Error('arguments must be similar to (fn, args...).') var fn = args.shift(); if(typeof fn !== 'function') throw new Error('fn must be a function.'); var func = function(){ return fn.apply(null, Array.prototype.slice.call(arguments).concat(args)); } return func; }; |
它假设第一个参数一定是个Function,其余就是要注入的参数,对这个Function进行一次包装并返回(闭包应用),需要注意的是 fn.apply() 中的那个arguments是 Function运行时的自有参数,而不是injectArgs函数的参数。
假设有一个被调函数funcA:
1 2 3 4 5 6 7 8 |
var funcA = function(){ var args = Array.prototype.slice.call(arguments); if(args.length < 1) throw new Error('callback does not exist.') var callback = args.pop(); if(typeof callback === 'function') callback.apply(null, args); }; |
它假设至少会有一个callback被传入,然后遵守约定,执行它。
那么,我们可以这么玩儿:
1 2 |
var funcAInected = injectArgs(funcA, callback); funcAInected(7, 8, 9); |
funcAInected 这个 funcA 被 injectArgs 包装后的代理函数,成功的将它的实际参数传递给了callback,它的执行链是:
funcA -> 传递funcA.arguments -> 转为callback.arguments -> callback执行 -> 返回 funcA。