我知道WeakMap
和WeakSet
出于安全原因不可迭代,即">防止攻击者看到垃圾收集器的内部行为",但是,这意味着您无法克隆WeakMap
或WeakSet
克隆Map
或Set
的方式,cloned_map = new Map(existing_map), cloned_set = new Set(existing_set)
。
如何在 Javascript 中克隆WeakMap
或WeakSet
?通过克隆,我的意思是使用相同的弱引用创建另一个WeakMap
或WeakSet
。
为什么WeakMap
/WeakSet
不是"可克隆的"?
WeakMap
s 和WeakSet
s 不是"可克隆的",原因与无法迭代它们的原因相同。
即避免暴露密钥无法访问的时间与从WeakMap
/WeakSet
中删除密钥之间的延迟。(这样做的原因已经涵盖了您的链接问题)
ECMAScript 2023 语言规范,24.3 弱映射对象
实现可能会在弱映射的键/值对变得不可访问的时间与从弱映射中删除键/值对的时间之间施加任意确定的延迟。如果 ECMAScript 程序可以观察到这种延迟,那么它将是可能影响程序执行的不确定性来源。出于这个原因,ECMAScript 实现不能提供任何方法来观察 WeakMap 的键,而弱映射不需要观察者提供观察到的键。
迭代性和可克隆性是如何联系在一起的
考虑如何实现new WeakMap(existingWeakMap)
.
要从现有WeakMap
创建新需要迭代其元素并将它们复制到新元素。
并且根据WeakMap
中有多少元素,此操作将花费不同的时间(复制具有 100'000 个条目的WeakMap
比复制一个没有条目的花费更长的时间)。
这为您提供了一个攻击媒介:您可以通过测量克隆它所需的时间来猜测 WeakMap 中的键值对数量。
这是一个可运行的片段,它使用这种技术来猜测Map
中的条目数(如果它是可克隆的,可以很容易地用于WeakMap
):
请注意,由于 Spectre 缓解措施,浏览器中performance.now()
通常是四舍五入的,因此猜测中的误差幅度应该更大。
function measureCloneTime(map) {
const begin = performance.now();
const cloneMap = new Map(map);
const end = performance.now();
return end-begin;
}
function measureAvgCloneTime(map, numSamples = 50) {
let timeSum = 0;
for(let i = 0; i < numSamples; i++) {
timeSum += measureCloneTime(map);
}
return timeSum / numSamples;
}
function makeMapOfSize(n) {
return new Map(Array(n).fill(null).map(() => [{}, {}]));
}
// prime JIT
for(let i = 0; i < 10000; i++) {
measureAvgCloneTime(makeMapOfSize(50));
}
const avgCloneTimes = [
{size: 2**6, time: measureAvgCloneTime(makeMapOfSize(2**6))},
{size: 2**7, time: measureAvgCloneTime(makeMapOfSize(2**7))},
{size: 2**8, time: measureAvgCloneTime(makeMapOfSize(2**8))},
{size: 2**9, time: measureAvgCloneTime(makeMapOfSize(2**9))},
{size: 2**10, time: measureAvgCloneTime(makeMapOfSize(2**10))},
{size: 2**11, time: measureAvgCloneTime(makeMapOfSize(2**11))},
{size: 2**12, time: measureAvgCloneTime(makeMapOfSize(2**12))},
{size: 2**13, time: measureAvgCloneTime(makeMapOfSize(2**13))},
{size: 2**14, time: measureAvgCloneTime(makeMapOfSize(2**14))},
];
function guessMapSizeBasedOnCloneSpeed(map) {
const cloneTime = measureAvgCloneTime(map);
let closestMatch = avgCloneTimes.find(e => e.time > cloneTime);
if(!closestMatch) {
closestMatch = avgCloneTimes[avgCloneTimes.length - 1];
}
const sizeGuess = Math.round(
(cloneTime / closestMatch.time) * closestMatch.size
);
console.log("Real Size: " + map.size + " - Guessed Size: " + sizeGuess);
}
guessMapSizeBasedOnCloneSpeed(makeMapOfSize(1000));
guessMapSizeBasedOnCloneSpeed(makeMapOfSize(4000));
guessMapSizeBasedOnCloneSpeed(makeMapOfSize(6000));
guessMapSizeBasedOnCloneSpeed(makeMapOfSize(10000));
在我的机器(Ubuntu 20,Chrome 107)上,我得到了以下输出(YMMV):
Real Size: 1000 - Guessed Size: 1037
Real Size: 4000 - Guessed Size: 3462
Real Size: 6000 - Guessed Size: 6329
Real Size: 10000 - Guessed Size: 9889
如您所见,仅通过克隆Map
就可以非常容易地猜测它的大小。(通过改进算法/采集更多样本/使用更准确的时间源,可以使其更加准确)
这就是为什么你不能克隆WeakMap
/WeakSet
.
可能的替代方案
如果您需要可克隆/可迭代WeakMap
/WeakSet
您可以使用WeakRef
和FinalizationRegistry
构建自己的。
下面是如何构建可迭代WeakMap
的示例:
class IterableWeakMap {
#weakMap = new WeakMap();
#refSet = new Set();
#registry = new FinalizationRegistry(this.#cleanup.bind(this));
#cleanup(value) {
this.#refSet.delete(value);
}
constructor(iterable) {
if(iterable) {
for(const [key, value] of iterable) {
this.set(key, value);
}
}
}
get(key) {
return this.#weakMap.get(key)?.value;
}
has(key) {
return this.#weakMap.has(key);
}
set(key, value) {
let entry = this.#weakMap.get(key);
if(!entry) {
const ref = new WeakRef(key);
this.#registry.register(key, ref, key);
entry = {ref, value: null};
this.#weakMap.set(key, entry);
this.#refSet.add(ref);
}
entry.value = value;
return this;
}
delete(key) {
const entry = this.#weakMap.get(key);
if(!entry) {
return false;
}
this.#weakMap.delete(key);
this.#refSet.delete(entry.ref);
this.#registry.unregister(key);
return true;
}
clear() {
for(const ref of this.#refSet) {
const el = ref.deref();
if(el !== undefined) {
this.#registry.unregister(el);
}
}
this.#weakMap = new WeakMap();
this.#refSet.clear();
}
*entries() {
for(const ref of this.#refSet) {
const el = ref.deref();
if(el !== undefined) {
yield [el, this.#weakMap.get(el).value];
}
}
}
*keys() {
for(const ref of this.#refSet) {
const el = ref.deref();
if(el !== undefined) {
yield el;
}
}
}
*values() {
for(const ref of this.#refSet) {
const el = ref.deref();
if(el !== undefined) {
yield this.#weakMap.get(el).value;
}
}
}
forEach(callbackFn, thisArg) {
for(const [key, value] of this.entries()) {
callbackFn.call(thisArg, value, key, this);
}
}
[Symbol.iterator]() {
return this.entries();
}
get size() {
let size = 0;
for(const key of this.keys()) {
size++;
}
return size;
}
static get [Symbol.species]() {
return IterableWeakMap;
}
}
// Usage Example:
let foo = {foo: 42};
let bar = {bar: 42};
const map = new IterableWeakMap([
[foo, "foo"],
[bar, "bar"]
]);
const clonedMap = new IterableWeakMap(map);
console.log([...clonedMap.entries()]);
这是可以做到的,相信你在通过跟踪WeakMap.prototype.set
和WeakMap.prototype.delete
制作弱图之前运行你的代码
但是,创建克隆需要我保持自己的事物观点,因此这可能会导致垃圾;-;不会收集任何弱图
。
//the code you run first
(()=>{
let MAPS=new Map()
let DELETE=WeakMap.prototype.delete, SET=WeakMap.prototype.set
let BIND=Function.prototype.call.bind(Function.prototype.bind)
let APPLY=(FN,THIS,ARGS)=>BIND(Function.prototype.apply,FN)(THIS,ARGS)
WeakMap.prototype.set=
function(){
let theMap=MAPS.get(this)
if(!theMap){
theMap=new Map()
MAPS.set(this,theMap)
}
APPLY(theMap.set,theMap,arguments)
return APPLY(SET,this,arguments)
}
WeakMap.prototype.delete=
function(){
let theMap=MAPS.get(this)
if(!theMap){
theMap=new Map()
MAPS.set(this,theMap)
}
APPLY(theMap.delete,theMap,arguments)
return APPLY(DELETE,this,arguments)
}
function cloneWM(target){
let theClone=new WeakMap()
MAPS.get(target).forEach((value,key)=>{
APPLY(SET,theClone,[key,value])
})
return theClone
}
window.cloneWM=cloneWM
})()
//the example(go on devtools console to see it properly)
let w=new WeakMap()
w.set({a:1},'f')
w.set({b:2},'g')
w.set(window,'a')
w.delete(window)
console.log([w,cloneWM(w)])
console.log("go on devtools console to see it properly")