Javascript装饰器与转发, call/apply

来源:互联网 发布:战地使命召唤知乎 编辑:程序博客网 时间:2024/05/22 11:37

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 cachedalert( "Again: " + slow(1) ); // the samealert( slow(2) ); // slow(2) is cachedalert( "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 cachinglet 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 beforefunction 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 worksworker.slow = cachingDecorator(worker.slow); // now make it cachingalert( 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 ); // JohnsayHi.call( admin ); // Admin

这里我们使用call调用say,使用给定的上下文和其他参数:

function say(phrase) {  alert(this.name + ': ' + phrase);}let user = { name: "John" };// user becomes this, and "Hello" becomes the first argumentsay.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 cachingalert( worker.slow(2) ); // worksalert( 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 callsworker.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 operatorfunc.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) ); // worksalert( "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参数对象。

还有很多其他的装饰器,可以尝试找到它们并应用解决本章任务。

原创粉丝点击