閱讀前,請先封印以下能力:類、閉包、繼承&多態、高階函數……
現在,你只會全局變量和函數,開始寫一個帶 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);
函數式是靠高階函數「復用代碼塊的流程」的,之前寫過一篇高階函數的博客,這里就不贅述了,感興趣的同學可以點這里。
最后,把面向對象和函數式放到表格里對比一下:
問題 | 面向對象 | 函數式 |
---|---|---|
消除全局變量 | 類&對象 | 閉包 |
復用代碼 | 繼承&多態 | 高階函數 |
盡管面向對象和函數式代碼表現形式不一樣,但解決的問題卻是同樣的。