Deep JavaScript
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

15 Unveränderliche Wrapper für Sammlungen



Ein unveränderlicher Wrapper für eine Sammlung macht diese Sammlung unveränderlich, indem er sie in ein neues Objekt verpackt. In diesem Kapitel untersuchen wir, wie das funktioniert und warum es nützlich ist.

15.1 Objekte wrappen

Wenn wir die Schnittstelle eines Objekts einschränken möchten, können wir folgenden Ansatz verfolgen:

So sieht das Wrapping aus

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

Verwandte Entwurfsmuster

15.1.1 Sammlungen durch Wrapping unveränderlich machen

Um eine Sammlung unveränderlich zu machen, können wir Wrapping verwenden und alle destruktiven Operationen aus ihrer Schnittstelle entfernen.

Ein wichtiger Anwendungsfall für diese Technik ist ein Objekt, das eine interne veränderliche Datenstruktur hat, die es sicher exportieren möchte, ohne sie zu kopieren. Der Export als "live" kann auch ein Ziel sein. Das Objekt kann seine Ziele erreichen, indem es die interne Datenstruktur wrappt und sie unveränderlich macht.

Die nächsten beiden Abschnitte stellen unveränderliche Wrapper für Maps und Arrays vor. Sie haben beide die folgenden Einschränkungen:

15.2 Ein unveränderlicher Wrapper für Maps

Die Klasse ImmutableMapWrapper erstellt Wrapper für 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();

Die Einrichtung des Prototyps muss über eine statische Methode erfolgen, da wir nur von innerhalb der Klasse auf das private Feld .#wrappedMap zugreifen können.

Dies ist ImmutableMapWrapper in Aktion

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 Ein unveränderlicher Wrapper für Arrays

Für ein Array arr reicht normales Wrapping nicht aus, da wir nicht nur Methodenaufrufe abfangen müssen, sondern auch den Zugriff auf Eigenschaften wie arr[1] = true. JavaScript Proxies ermöglichen uns dies.

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

Lasst uns ein Array wrappen

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