Как клонировать «WeakMap» или «WeakSet» в Javascript?

Я знаю, что WeakMap и WeakSet не являются итерируемыми по соображениям безопасности, то есть «чтобы злоумышленники не видели внутреннее поведение сборщика мусора », но тогда это означает, что вы не можете клонировать WeakMap или WeakSet так, как вы клонируете Map или Set, cloned_map = новая карта (существующая_карта), cloned_set = новый набор (существующий_набор).

Как клонировать WeakMap или WeakSet в Javascript? Под клонированием я подразумеваю создание другого WeakMap или WeakSet с такими же слабыми ссылками.

Может быть, вы могли бы использовать Object.create?

andriusain 29.11.2022 08:40

Их невозможно клонировать, и точка. Их цель — скрыть прямой доступ к ключам и значениям.

vitaly-t 30.11.2022 15:24

@vitaly-t Хотя в настоящее время это верно, невозможность доступа к ключам не может быть причиной невозможности клонирования (т. Е. Логически несвязанной).

Константин Ван 30.11.2022 15:31

Зачем кому-то клонировать структуру данных, о которой заботится сборщик мусора и где последняя, ​​возможно, является одним большим аргументом в пользу существования WeakMap и WeakSet? Каждый клон дублирует ссылки, что может усложнить механизм очистки, чтобы выяснить, какие ссылки действительно «мертвые».

Peter Seliger 30.11.2022 15:45

@PeterSeliger Под клонированием я подразумеваю клонирование экземпляра Weak* в другой экземпляр Weak*, а не что-то вроде .entries(), .keys() или .values(). Копирование слабых ссылок и сопоставлений без раскрытия поведения сборщика мусора. Все экземпляры Weak* управляются GC одинаково, и слабые ссылки мертвы, когда цели, на которые они указывают, мертвы; Я не совсем понимаю, как наличие нескольких слабых ссылок на одну и ту же цель, что уже возможно, усложняет что-либо.

Константин Ван 30.11.2022 18:23

Я сказал, что доступ к ключам и клонирование «логически не связаны», потому что клонирование может быть выполнено без предоставления пользователю доступа к ключам и значениям и раскрытия того, когда сборщик мусора собрал что. Эти два понятия просто разные, не связанные.

Константин Ван 30.11.2022 20:34
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
1
6
146
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Это можно сделать, полагая, что вы запускаете свой код до того, как что-то создаст слабую карту, отслеживая 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.info([w,cloneWM(w)])
console.info("go on devtools console to see it properly")

тому, кто поставил минус 1 на мой пост, пока это не идеальный вопрос, это рабочий ответ

The Bomb Squad 30.11.2022 19:56

Если вы храните ключи в отдельном Map, то всегда будет строгая ссылка на этот ключ (т. е. он никогда не будет подвергаться сборке мусора), так что это в первую очередь противоречит цели использования WeakMap.

Turtlefight 05.12.2022 21:32
Ответ принят как подходящий

Почему WeakMap/WeakSet нельзя «клонировать»?

WeakMaps и WeakSets нельзя «клонировать» по той же причине, по которой вы не можете их повторять.

А именно, чтобы не выставлять задержку между моментом, когда ключ становится недоступным, и когда он удаляется из WeakMap / WeakSet. (Причина этого уже рассмотрена ваш связанный вопрос)

Спецификация языка ECMAScript 2023, объекты WeakMap 24.3

Реализация может устанавливать произвольно определяемую задержку между моментом, когда пара ключ/значение в WeakMap становится недоступной, и моментом, когда пара ключ/значение удаляется из WeakMap. Если бы эту задержку можно было наблюдать в программе ECMAScript, это было бы источником неопределенности, которая могла бы повлиять на выполнение программы. По этой причине реализация ECMAScript не должна предоставлять какие-либо средства для наблюдения за ключом WeakMap, которые не требуют от наблюдателя представления наблюдаемого ключа.


Как связаны итерируемость и клонируемость

Подумайте, как new WeakMap(existingWeakMap) нужно реализовать.
Чтобы создать новый WeakMap из существующего, потребуется перебрать его элементы и скопировать их в новый.

И в зависимости от того, сколько элементов содержится в WeakMap, эта операция займет разное количество времени (копирование WeakMap со 100 000 записей займет намного больше времени, чем копирование без них).

И это дает вам вектор атаки: вы можете приблизительно оценить количество пар ключ-значение в WeakMap, измерив, сколько времени требуется для его клонирования.

Вот работающий фрагмент, который использует эту технику для угадывания количества записей в Map (можно было бы легко использовать против WeakMap, если бы его можно было клонировать):

Note that due to Spectre mitigations performance.now() in browsers is typically rounded, so a larger margin of error in the guesses should be expected.

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.info("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.info([...clonedMap.entries()]);

«Клонирование не является защитой от атаки по времени и, таким образом, нарушает спецификацию ECMAScript».... Об этом я не подумал, и это довольно справедливое замечание! Спасибо за ваши идеи.

Константин Ван 06.12.2022 03:04

Другие вопросы по теме