阅读前,请先封印以下能力:类、闭包、继承&多态、高阶函数……

现在,你只会全局变量和函数,开始写一个带 cache 的 Fibonacci。

const cache = new Map();

const fib = n => {
  if (cache.has(n)) {
    console.log("use cache", n);
    return cache.get(n);
  } else {
    let result;
    if (n === 1 || n === 2) result = 1;
    else result = fib(n - 1) + fib(n - 2);
    cache.set(n, result);
    return result;
  }
};

fib(10);

再要求你写几十个类似的函数,你会陷入两难的境地:是把全局变量定义在操作它的函数附近,还是把全体全局变量定义在一处好?

  • 把全局变量定义在操作它的函数附近,容易因为变量名冲突造成程序错误。
  • 把全局变量定义在一处,代码不好拆分成独立文件,导致不好复用。

引入命名空间是缓解全局变量污染的解法,使用面向对象的类是消除全局变量的解法。

类把变量和操作变量的函数聚在一起,变量不再是全局的,从而减少了全局变量。

class FibCalculator {
  #cache = new Map();

  calc(n) {
    if (this.#cache.has(n)) {
      console.log("use cache", n);
      return this.#cache.get(n);
    } else {
      let result;
      if (n === 1 || n === 2) result = 1;
      else
        result = this.calc(n - 1) + this.calc(n - 2);
      this.#cache.set(n, result);
      return result;
    }
  }
}

const fib = new FibCalculator();
fib.calc(10);

函数的闭包也一样,把变量和操作变量的函数聚在一起,变量不再是全局的。

const fib = (function () {
  const cache = new Map();

  const fib = n => {
    if (cache.has(n)) {
      console.log("use cache", n);
      return cache.get(n);
    } else {
      let result;
      if (n === 1 || n === 2) result = 1;
      else result = fib(n - 1) + fib(n - 2);
      cache.set(n, result);
      return result;
    }
  };

  return fib;
})();

闭包等价于「只有一个函数的对象」,可以用闭包替代下图中的 class Aclass B


类、闭包解决了全局变量的问题,我们再来谈代码复用的问题,有两种复用:

  1. 复用整个代码块
  2. 复用代码块的流程

还以这段 Fibonacci 为例:

class FibCalculator {
  #cache = new Map();

  calc(n) {
    if (this.#cache.has(n)) {
      console.log("use cache", n);
      return this.#cache.get(n);
    } else {
      let result;
      if (n === 1 || n === 2) result = 1;
      else
        result = this.calc(n - 1) + this.calc(n - 2);
      this.#cache.set(n, result);
      return result;
    }
  }
}

const fib = new FibCalculator();
fib.calc(10);

程序需要计算 Fibonacci 时,可以导入 class, new 出实例,实现复用,这个复用就是「复用整个代码块」。

另外我们发现,不管是计算 Fibonacci 还是计算 Factorial, cache 的逻辑都是一样的:

  • 添加一个 cache 私有变量
  • 计算前先看 cache 中有没有
    • 有就直接返回
    • 没有则计算,计算完了存入 cache,再返回

复用 cache 的逻辑就是我说的「复用代码块的流程」。

面向对象是靠继承&多态实现「复用代码块的流程」的。

class Calculator {
  calc(n) {}
}

class CachedCalculator extends Calculator {
  #cache = new Map();
  #calculator;
  constructor(calculator) {
    super();
    this.#calculator = calculator;
  }
  calc(n) {
    if (this.#cache.has(n)) {
      console.log("use cache", n);
      return this.#cache.get(n);
    } else {
      const result = this.#calculator.calc(n);
      this.#cache.set(n, result);
      return result;
    }
  }
}

class FibCalculator extends Calculator {
  calc(n) {
    if (n === 1 || n === 2) return 1;
    else return this.calc(n - 1) + this.calc(n - 2);
  }
}

class FactorialCaculator extends Calculator {
  calc(n) {
    if (n === 1) return 1;
    else return n * this.calc(n - 1);
  }
}

const fib = new CachedCalculator(
  new FibCalculator()
);
fib.calc(10);

const factorial = new CachedCalculator(
  new FactorialCaculator()
);
factorial.calc(10);

有些看官也许看出这版 cache 有问题,递归的部分并没有存入 cache。计算 fib.calc(10),按理说,1-9 都计算了一遍,但 cache 中只存了 10 的结果。代码改进一下,就可以让递归的部分也存入 cache。

class Calculator {
  calc(n, self) {}
}

class CachedCalculator extends Calculator {
  #cache = new Map();
  #calculator;
  constructor(calculator) {
    super();
    this.#calculator = calculator;
  }
  calc(n, self = null) {
    if (this.#cache.has(n)) {
      console.log("use cache", n);
      return this.#cache.get(n);
    } else {
      const result = this.#calculator.calc(n, this);
      this.#cache.set(n, result);
      return result;
    }
  }
}

class FibCalculator extends Calculator {
  calc(n, self) {
    if (n === 1 || n === 2) return 1;
    else return self.calc(n - 1) + self.calc(n - 2);
  }
}

class FactorialCaculator extends Calculator {
  calc(n, self) {
    if (n === 1) return 1;
    else return n * self.calc(n - 1);
  }
}

const fib = new CachedCalculator(
  new FibCalculator()
);
fib.calc(10);

const factorial = new CachedCalculator(
  new FactorialCaculator()
);
factorial.calc(10);

函数式是靠高阶函数「复用代码块的流程」的,之前写过一篇高阶函数的博客,这里就不赘述了,感兴趣的同学可以点这里


最后,把面向对象和函数式放到表格里对比一下:

问题 面向对象 函数式
消除全局变量 类&对象 闭包
复用代码 继承&多态 高阶函数

尽管面向对象和函数式代码表现形式不一样,但解决的问题却是同样的。

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/apolis/p/14592408.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程