深入 JavaScript
請支持這本書:購買捐款
(廣告,請不要封鎖。)

15 收藏的不可變包裝器



收藏的不可變包裝器會將收藏包裝在新的物件中,讓該收藏不可變。在本章中,我們將探討這種運作方式以及它的用途。

15.1 包裝物件

如果有一個物件的介面我們想要縮減,我們可以採取下列方法

包裝的樣子如下

class Wrapper {
  #wrapped;
  constructor(wrapped) {
    this.#wrapped = wrapped;
  }
  allowedMethod1(...args) {
    return this.#wrapped.allowedMethod1(...args);
  }
  allowedMethod2(...args) {
    return this.#wrapped.allowedMethod2(...args);
  }
}

相關軟體設計模式

15.1.1 透過包裝讓收藏不可變

若要讓收藏不可變,我們可以使用包裝,並從它的介面中移除所有破壞性操作。

這種技術的一個重要使用案例是一個具有內部可變資料結構的物件,它希望在不複製的情況下安全地匯出該資料結構。匯出為「即時」也可能是目標。物件可以透過包裝內部資料結構並使其不可變來達成其目標。

接下來的兩個區塊展示了 Map 和陣列的不可變包裝器。它們都有下列限制

15.2 Maps 的不可變包裝器

類別 ImmutableMapWrapper 會產生 Maps 的包裝器

class ImmutableMapWrapper {
  static _setUpPrototype() {
    // Only forward non-destructive methods to the wrapped Map:
    for (const methodName of ['get', 'has', 'keys', 'size']) {
      ImmutableMapWrapper.prototype[methodName] = function (...args) {
        return this.#wrappedMap[methodName](...args);
      }
    }
  }

  #wrappedMap;
  constructor(wrappedMap) {
    this.#wrappedMap = wrappedMap;
  }
}
ImmutableMapWrapper._setUpPrototype();

原型設定必須由靜態方法執行,因為我們只能從類別內部存取私有欄位 .#wrappedMap

這是 ImmutableMapWrapper 的實際應用

const map = new Map([[false, 'no'], [true, 'yes']]);
const wrapped = new ImmutableMapWrapper(map);

// Non-destructive operations work as usual:
assert.equal(
  wrapped.get(true), 'yes');
assert.equal(
  wrapped.has(false), true);
assert.deepEqual(
  [...wrapped.keys()], [false, true]);

// Destructive operations are not available:
assert.throws(
  () => wrapped.set(false, 'never!'),
  /^TypeError: wrapped.set is not a function$/);
assert.throws(
  () => wrapped.clear(),
  /^TypeError: wrapped.clear is not a function$/);

15.3 陣列的不可變包裝器

對於陣列 arr,一般的包裝還不夠,因為我們需要攔截的不只是方法呼叫,還有屬性存取,例如 arr[1] = trueJavaScript 代理 能讓我們做到這一點

const RE_INDEX_PROP_KEY = /^[0-9]+$/;
const ALLOWED_PROPERTIES = new Set([
  'length', 'constructor', 'slice', 'concat']);

function wrapArrayImmutably(arr) {
  const handler = {
    get(target, propKey, receiver) {
      // We assume that propKey is a string (not a symbol)
      if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
        || ALLOWED_PROPERTIES.has(propKey)) {
          return Reflect.get(target, propKey, receiver);
      }
      throw new TypeError(`Property "${propKey}" can’t be accessed`);
    },
    set(target, propKey, value, receiver) {
      throw new TypeError('Setting is not allowed');
    },
    deleteProperty(target, propKey) {
      throw new TypeError('Deleting is not allowed');
    },
  };
  return new Proxy(arr, handler);
}

讓我們包裝一個陣列

const arr = ['a', 'b', 'c'];
const wrapped = wrapArrayImmutably(arr);

// Non-destructive operations are allowed:
assert.deepEqual(
  wrapped.slice(1), ['b', 'c']);
assert.equal(
  wrapped[1], 'b');

// Destructive operations are not allowed:
assert.throws(
  () => wrapped[1] = 'x',
  /^TypeError: Setting is not allowed$/);
assert.throws(
  () => wrapped.shift(),
  /^TypeError: Property "shift" can’t be accessed$/);