Skip to main content
 首页 » 编程设计

Javascript装饰器与转发, call/apply

2022年07月19日249三少

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同时设置thisobj

下面示例代码中,在不同的对象上下文中调用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); 

这里需解决两个任务。

首先如何使用两个参数minmax作为缓存map的键,前面是单个参数x,我们能直接通过cache.set(x,result)保存结果,然后使用cache.get(x)返回结果。但是我们需要使用复合参数记住结果(min,max).默认Map只能使用一个值作为键。

有多个解决方法:

  1. 实现一个新的类map数据结构,其更通用且支持多个键。
  2. 使用嵌套的map:cache.set(min)作为map对象存储在对应的(max,result)中。所以我们能这样获得结果:cache.get(min).get(max).
  3. 链接两个值为一个,在我们特定的示例中,可以仅使用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)的内部算法很简单,集合和规范一致:

  1. 设置 glue 作为第一个参数, 如果没有参数,那么为一个逗号 ",".
  2. 设置 result为一个空字符串.
  3. 追加 this[0]result.
  4. 追加 gluethis[1].
  5. 追加 gluethis[2].
  6. …一直做直到 this.length 个项目被加入.
  7. 返回 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