是否有可能使像WeakMap这样的东西是双向的(通过其键获取值,或通过其值获取键)?
的用法是这样的(在TypeScript语法中更好地说明):
class TwoWayWeakMap {
// What goes here?
}
class SomeClass {}
const map = new TwoWayWeakMap<SomeClass, number>()
const o = new SomeClass
map.set(o, 42)
console.log(map.get(o)) // logs "42"
console.log(map.keyFrom(42)) // logs "SomeClass {}" (the `o` object)
在以后的任何时刻,如果o
不再被引用,除了在TwoWayWeakMap
内部,那么o
所指向的SomeClass
对象可以被收集。
注意!map.set(k, v)
的第二个参数必须允许是任何东西,而不仅仅是对象。例如,v
可以是number
。
方法如下:
<script type=module>
let tick = 0
const loop = setInterval(() => {
const obj = window.map.keyFrom(42)
console.log(`o still exists? (${tick++})`, !!obj)
if (!obj) {
clearInterval(loop)
console.log('o was collected!')
}
}, 300)
</script>
<script type=module>
class TwoWayWeakMap /*<K extends object = object, V = unknown>*/
extends WeakMap /*<K, V>*/ {
#refs /*: Set<WeakRef>*/ = new Set();
constructor() {
super();
setInterval(() => this.maybeCleanup(), 1000);
}
set(k /*: K*/ , v /*: V*/ ) /*: void*/ {
super.set(k, v);
this.#refs.add(new WeakRef(k));
}
keyFrom(v /*: V*/ ) /*: K | undefined*/ {
for (const ref of this.#refs) {
const o = ref.deref();
if (!o) {
this.#refs.delete(ref);
continue;
}
if (this.get(o) === v) return o;
}
}
maybeCleanup() {
for (const ref of this.#refs) {
const o = ref.deref();
if (!o) this.#refs.delete(ref);
}
}
}
class SomeClass {}
function main() {
const map = (window.map = new TwoWayWeakMap /*<SomeClass, number>*/());
const o = new SomeClass();
map.set(o, 42);
console.log(map.get(o)); // logs "42"
console.log('Get object from key:', !!map.keyFrom(42)); // logs "true"
}
main();
// At this point there is no reference to `o`, except by
// WeakRef and WeakMap, so `o` should be collectable.
</script>
Kaiido提供了另一种方法,使用第二个映射来消除迭代的需要:
class TwoWayWeakMap extends WeakMap {
#reverseMap;
constructor( iterable ) {
super(iterable);
this.#reverseMap = new Map();
if (iterable) {
for (const [k,v] of iterable ) {
this.set(k,v);
}
}
}
set(k,v) {
super.set(k,v);
this.#reverseMap.set(v, new WeakRef(k));
}
keyFrom(v) {
const k = this.#reverseMap.get(v)?.deref();
if (!k) { // suboptimal clean value at getting...
this.#reverseMap.delete(v);
};
return k;
}
}
class SomeClass {}
const map = new TwoWayWeakMap();
{
const o = new SomeClass();
map.set(o, 42)
console.log(map.get(o)) // logs "42"
console.log(map.keyFrom(42) === o) // logs "SomeClass {}" (the `o` object)
}
// check it gets collected, eventually
// convert to Boolean to avoid the console keeping an hard reference
setInterval(() => console.log(!!map.keyFrom(42)), 1000 );
FinalizationRegistry使它更完美!
class TwoWayWeakMap<K extends object, V extends any = any> {
_map = new WeakMap<K, V>()
_keyMap = new Map<V, WeakRef<K>>()
_registry: FinalizationRegistry<V>
constructor() {
this._registry = new FinalizationRegistry<V>((key) => {
this._keyMap.delete(key)
})
}
set(key: K, value: V) {
this._map.set(key, value)
this._keyMap.set(value, new WeakRef(key))
this._registry.register(key, value)
}
get(key: K): V | undefined {
return this._map.get(key)
}
has(key: K): boolean {
return this._map.has(key)
}
keyFrom(value: V): K | undefined {
const ref = this._keyMap.get(value)
return ref?.deref()
}
}
async function main() {
const map = new TwoWayWeakMap()
let data = { hello: "world!" } as any
map.set(data, "something!")
console.log('---before---')
console.log(map.keyFrom("something!"))
data = null
await new Promise((resolve) => setTimeout(resolve, 0))
global.gc() // call gc manually
await new Promise((resolve) => setTimeout(resolve, 0))
console.log('---after--')
console.log(map.keyFrom("something!"))
}
main()
它必须在node.js环境中使用--expose-gc
选项运行。
似乎不应该比
更复杂class TwoWayWeakMap extends WeakMap {
constructor(iterable) {
super(iterable);
if (iterable) {
for (const [k,v] of iterable ) {
this.set(k,v);
}
}
}
set(k,v) {
super.set(k,v);
super.set(v,k);
}
}
警告
这里有一个隐含的假设,即键和值的集合并集本身形成一个集合(也就是说,键的集合本身是唯一的,值的集合本身也是唯一的,并且两者之间存在共性)。
如果
k
或v
是对象,由于Set
,Map
,WeakSet
和WeakMap
的方法使用引用相等性,因此只有完全相同的对象将匹配。其他任何内容,即使是完全重复也不会匹配。