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