面向對象和函數式


閱讀前,請先封印以下能力:類、閉包、繼承&多態、高階函數……

現在,你只會全局變量和函數,開始寫一個帶 cachefibonacci

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);

再要求你寫幾十個類似的函數,你會陷入兩難的境地:是把全局變量定義在操作它的函數附近,還是把全體全局變量定義在一處好?

  • 把全局變量定義在操作它的函數附近,容易因為變量名沖突造成程序錯誤。
  • 把全局變量定義在一處,代碼不好拆分成獨立文件,導致不好復用。

chaos

引入命名空間是緩解全局變量污染的解法,使用面向對象的類是消除全局變量的解法。

類把變量和操作變量的函數聚在一起,變量不再是全局的,從而減少了全局變量。

class

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

class-closure


類、閉包解決了全局變量的問題,我們再來談代碼復用的問題,有兩種復用:

  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);

函數式是靠高階函數「復用代碼塊的流程」的,之前寫過一篇高階函數的博客,這里就不贅述了,感興趣的同學可以點這里


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

問題 面向對象 函數式
消除全局變量 類&對象 閉包
復用代碼 繼承&多態 高階函數

盡管面向對象和函數式代碼表現形式不一樣,但解決的問題卻是同樣的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM