阅读前,请先封印以下能力:类、闭包、继承&多态、高阶函数……
现在,你只会全局变量和函数,开始写一个带 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 A
和 class B
。
类、闭包解决了全局变量的问题,我们再来谈代码复用的问题,有两种复用:
- 复用整个代码块
- 复用代码块的流程
还以这段 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);
函数式是靠高阶函数「复用代码块的流程」的,之前写过一篇高阶函数的博客,这里就不赘述了,感兴趣的同学可以点这里。
最后,把面向对象和函数式放到表格里对比一下:
问题 | 面向对象 | 函数式 |
---|---|---|
消除全局变量 | 类&对象 | 闭包 |
复用代码 | 继承&多态 | 高阶函数 |
尽管面向对象和函数式代码表现形式不一样,但解决的问题却是同样的。