Appearance
目录:
对集合的不可变包装器使集合不可变,通过将其包装到一个新的对象中。
本章,我们将看看它是如何工作的,以及不可变包装的用处。
1️⃣ 包装对象
如果某个对象我们想减少其接口,我们可以采取下面方式:
- 创建一个新对象,将原对象存储在一个私有字段中。📚新对象即 包装(
wrap
) 了原对象。新对象也称之为 包装器(wrapper
),而原对象称之为 被包装的对象(wrapped object
) - 包装器只将它接收到的一些方法调用转发给被包装的对象。
👇🏻下面是包装的方式:
js
class Wrapper { // 包装器
#wrapped // 被包装的对象,即原对象
constructor(wrapped) {
this.#wrapped = wrapped
}
allowedMethod1(...args) {
return this.#wrapped.allowedMethod1(...args)
}
allowMethod2(...args) {
return this.#wrapped.allowedMethod2(...args)
}
}
1.1 通过包装使集合不可变
📚 为了使集合不可变,我们可以使用包装,将接口中所有破坏性(destructive
)操作都移除。
这种技术的一个重要使用场景是,某个对象有个内部可变数据结构,对象想不使用拷贝的方式安全的导出该数据。导出是“实时”的,可能也是一个目标。对象可以通过包装该内部数据结构,并将其变为不可变完成这些目标。
下面2个小节展示对 Maps
& Arrays
的不可变包装器。它们都存在下面这些限制:
- 它们只是粗略的实现。要使它们适合实际使用,还需要做更多的工作:更好的检查,支持更多的方法,等等。
- 它们的工作方式很简单: 每一种方法都使包装的对象不可变,但不是它返回的数据不可变。可以通过包装方法返回的一些结果来解决这个问题。
2️⃣ 对Maps的不可变包装器
类 ImmutableMapWrapper
对Maps产生包装器:
js
class ImmutableMapWrapper {
static _setUpPrototype() {
// 💡只转发非破坏性方法给包装的Map
for (const methodName of ['get', 'has', 'keys', 'size']) {
ImmutableMapWrapper.prototype[methodName] = function (...args) {
return this.#wrappedMap[methodName](...args)
}
}
}
#wrappedMap // 被包装的Map
constructor(wrappedMap) {
this.#warppedMap = wrappedMap
}
}
ImmutableMapWrapper._setUpPrototype()
WARNING
设置原型必须通过静态方法执行,因为我们只能从类内部访问私有字段 #wrappedMap
。
使用 ImmutableMapWrapper
:
js
const map = new Map([
[false, 'no'],
[true, 'yes']
])
const wrapped = new ImmutableMapWrapper(map)
// 非破坏性操作正常工作 😎
assert.equal(wrapped.get(true), 'yes')
assert.equal(wrapped.has(false), true)
assert.deepEqual([...wrapped.keys()], [false, true])
// 破坏性操作不可访问 🚫
assert.throws(
() => wrapped.set(false, 'never!'),
/^TypeError: wrapped.set is not a function$/
)
assert.throws(
() => wrapped.clear(),
/^TypeError: wrapped.clear is not a function$/
)
3️⃣ 对Arrays的不可变包装器
对数组 arr
,普通包装是不够的,因为我们不仅需要拦截方法调用,也要拦截属性访问,比如 arr[1] = true
。JS Proxies 允许我们达到这一目标:
js
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) {
// 假设 propKey 是字符串,而不是symbols
if (
RE_INDEX_PROP_KEY.test(propKey)
|| 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)
}
让我们包装数组:
js
const arr = ['a', 'b', 'c']
const wrapped = wrapArrayImmutably(arr)
// 允许非破坏性操作
assert.deepEqual(
wrapped.slice(1),
['b', 'c']
)
assert.equal(wrapped[1], 'b')
// 不允许破坏性操作 🚫
assert.throws(
() => wrapped[x] = 'x',
/^TypeError: Setting is not allowed$/
)
assert.throws(
() => wrapped.shift(),
/^TypeError: Property "shift" can’t be accessed$/
)
4️⃣ 总结(译者注)
- 包装器是什么,被包装是什么意思
- 包装器的本质是什么?使用一个对象代理包装另一个对象
- 包装器涉及到的设计模式:代理模式,门面模式
- 不可变的含义是什么?非破坏性的改变数据结构
- 不可变包装器:包装器只导出非破坏性(不可变)操作,对可变操作进行控制
- 对对象的包装:使用私有字段关联要包装的对象
- 对Maps的包装:类似对象包装,使用静态方法
- 对Arrays的包装:使用
Proxies
对数据进行代理,并只保留非破坏性操作
原文链接:
2022年07月28日00:42:16