深入理解 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 通过包装使集合不可变

为了使集合不可变,我们可以使用包装并从其接口中删除所有破坏性操作。

此技术的一个重要用例是一个对象,它有一个内部可变数据结构,它希望安全地导出该结构而不复制它。“实时”导出也可能是一个目标。该对象可以通过包装内部数据结构并使其不可变来实现其目标。

接下来的两节展示了 Maps 和 Arrays 的不可变包装器。它们都有以下限制

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 Arrays 的不可变包装器

对于数组 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$/);