装饰器 | 青训营笔记

109 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第1天

在前端课程的「跟着月影学 JavaScript」中,月影老师在过程抽象中提到了高阶函数以及常用的一些高阶函数和装饰器的原理。然而我在自己尝试实现一个装饰器时碰到了一个常见的this传递错误的问题,于是就有了这篇笔记来总结装饰器中正确传递this的方法。

什么是装饰器

装饰器是一个特殊的高阶函数,它接受另一个函数并改变它的行为。

高阶函数是满足以下任意条件之一的函数

  • 以函数作为参数
  • 以函数作为返回值

当一个函数同时满足以上两个条件时,我们可以称这个函数为装饰器。使用装饰器,我们可以完成对任意一个函数的转发和装饰。

下面是一个装饰器的示例:

function pow2(x){
  console.log(`calc ${2} ^ 2`);
  return x * x;
}

function cacheDecorator(fn){
  let cache = new Map();
  return function(x){
    if(cache.has(x)){
      return cache.get(x);
    }
    let res = fn(x);
    cache.set(x, res);
    return res;
  }
}

let pow2WithCache = cacheDecorator(pow2);
console.log(pow2WithCache(2));
console.log(pow2WithCache(2));

在上面的代码中,我们定义了一个缓存装饰器cacheDecorator, 在某个函数被频繁调用且根据输入的参数输出的结果是一个稳定的结果是,我们就可以使用缓存将输入和输出给缓存起来,避免了在重新计算上花费的时间。

装饰器中的this

实际上我们在使用装饰器转发一个类中的函数的时候,如果函数内用到了this,那么这个this指向的是哪里呢?

class Pow {

  constructor(base) {
    this.base = base;
  }

  calc(x){
    console.log(`calc ${x} ^ ${this.base}`);
    return Math.pow(x, this.base);
  }
}

function cacheDecorator(fn){
  let cache = new Map();
  return function(x){
    if(cache.has(x)){
      return cache.get(x);
    }
    let res = fn(x); // (1)
    cache.set(x, res);
    return res;
  }
}

let pow2 = new Pow(2);
console.log(pow2.calc(2)); // 4

pow2.calc = cacheDecorator(pow2.calc);

console.log(pow2.calc(2)); //Cannot read properties of undefined (reading 'base')

在使用装饰器装饰一个函数时,由于调用原函数 (1) 的过程中函数前并不包含pow2.,所以此时在函数内部的this就会变为undfined。

要解决这个问题,我们可以在调用原函数时,使用func.call或者func.apply来显式将this传递给原函数。 我们将调用原函数的语句(1)let res = fn.call(this,x);,就能将this传递给原函数。pow2.calc(2)时也将不会发生错误。

在装饰器中正确传递this

在下面的代码中,我们将Pow类的calc方法修改为直接打印结果,并且编写了一个延时执行函数的装饰器。

class Pow {

  constructor(base) {
    this.base = base;
  }

  calc(x){
    console.log(Math.pow(x, this.base));
  }
}

function delay(fn, ms) {
  return function(x) {
    setTimeout(function(){
      fn.call(this, x);
    }, ms);
  };
}

let pow2 = new Pow(2);
pow2.calc(2); // 4

pow2.calc = delay(pow2.calc, 1000);

console.log(pow2.calc(2)); // NAN

在将函数calc使用延时装饰器装饰之后,我们发现this并没有被正确传递,原因是我们在传递一个函数给setTimeout时,setTimeout会不带参数直接调用并且将this指向window(在浏览器环境下)。

我们可以使用中间变量来解决这个问题。我们在传递一个函数给setTimeout时,可以使用一个中间变量t来正确传递外层函数的this。

function delay(fn, ms) {
  return function(x) {
    let t = this;
    setTimeout(function(){
      fn.call(t, x);
    }, ms);
  };
}

另一种解决办法是使用箭头函数。因为箭头函数并不存在自己的this,所以此时的this自然会变成外层函数的this。

function delay(fn, ms) {
  return function(x) {
    setTimeout(() => fn.call(this, x), ms);
  };
}

总结

  • 装饰器是同时满足参数为函数和返回值为函数的高阶函数。
  • 在装饰器中调用其他函数时,我们应该使用func.call或者func.apply来正确传递this。
  • 在装饰器中异步调用其他函数时,使用箭头函数可以避免this的的丢失。