收藏的不可變包裝器會將收藏包裝在新的物件中,讓該收藏不可變。在本章中,我們將探討這種運作方式以及它的用途。
如果有一個物件的介面我們想要縮減,我們可以採取下列方法
包裝的樣子如下
class Wrapper {
#wrapped;
constructor(wrapped) {
this.#wrapped = wrapped;
}
allowedMethod1(...args) {
return this.#wrapped.allowedMethod1(...args);
}
allowedMethod2(...args) {
return this.#wrapped.allowedMethod2(...args);
}
}
相關軟體設計模式
若要讓收藏不可變,我們可以使用包裝,並從它的介面中移除所有破壞性操作。
這種技術的一個重要使用案例是一個具有內部可變資料結構的物件,它希望在不複製的情況下安全地匯出該資料結構。匯出為「即時」也可能是目標。物件可以透過包裝內部資料結構並使其不可變來達成其目標。
接下來的兩個區塊展示了 Map 和陣列的不可變包裝器。它們都有下列限制
類別 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$/);
對於陣列 arr
,一般的包裝還不夠,因為我們需要攔截的不只是方法呼叫,還有屬性存取,例如 arr[1] = true
。 JavaScript 代理 能讓我們做到這一點
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$/);