Ключ к ключам: когда ключи JavaScript не совпадают

Разработчики JavaScript нередко превращают значения в ключи или ключи в значения, обычно для использования при кэшировании или индексировании. Обычный подход - просто преобразовать значения в строки, например:

const index = {};
const object = {id:123, name:"joe",age:27};
index[object.id] = object;

В этом случае JavaScript автоматически преобразует id в строку. Однако это означает, что что-то с другим, но принудительно тем же идентификатором может перезаписать значение индекса:

const index = {};
const object = {id:"123", name:"mary",age:26};
index[object.id] = object;

Если ваше приложение контролирует генерацию всех идентификаторов, вы можете избежать этого. Но что, если этого не происходит, или что если у вас другой вариант использования, например использовать аргументы функции для создания ключа мемоизации? Это может привести к появлению неприятных ошибок, которые сложно отследить. Ниже приводится тривиальная memoize функция, подверженная этому недостатку, и sum функция, аналогичная той, что находится в электронной таблице:

function memoize(f) {
  const memoized = function(...args) {
    const key = args.reduce((accum,key,i) => 
      accum+=key+(i<args.length-1 ? ":" : ""),"");
    return memoized[key] || (memoized[key] = f(...args));
  }
  return memoized;
}
function sum(...args) {
  return args.reduce((accum,value) => 
    typeof(value)==="number" ? accum+=value : accum,0);
}
console.log(sum(10,"100")); // expect 10, get 10
console.log(sum(10,100)); // expect 110, get 110
sum = memoize(sum);
console.log(sum(10,"100")); // expect 10, get 10
console.log(sum(10,100)); // expect 110, get 10!

Обратите внимание: хотя приведенное выше является тривиальным запоминанием, существуют популярные библиотеки запоминания, подверженные этому недостатку. Другой пример функции, которая не будет правильно запоминать в библиотеках, подверженных этому недостатку:

function kindOf (arg) {
  return (arg && typeof arg ==="object" 
   ? arg.constructor.name 
   : typeof arg)
}

Снова тривиально, но невозможно сказать, что может быть в недрах вашего или какого-либо другого кода.

Существует простое решение, в котором используется природа строк JSON.

const toKey = (value) => {
  const type = typeof(value);
  if(!value || type==="number" || type==="boolean) return value+"";
  return JSON.stringify(value);
}

Эта функция вернет 123 для числа 123, но вернет "123" для строки 123. Код ниже иллюстрирует разницу:

const test = {
  [toKey(123)]: 123,
  [toKey("123")]: "123"
}
console.log(test); // {123: "123", "123": "123"}
console.log(JSON.stringify(test)); // {"123":123,"\"123\"":"123"}

Обратите внимание на кавычки вокруг второго ключа выше.

Вам также может быть интересно, какого черта квадратные скобки используются при создании объекта. Это малоизвестная функция JavaScript, которая позволяет создавать свойства на основе оценки другого JavaScript… тема будущей статьи.

Хорошо, а что, если вы хотите отменить процесс и превратить ключ в его исходное значение? Просто используйте JSON.parse и полагайтесь на обработку ошибок, например.

const fromKey = (value) => {
  if(value==="undefined") return;
  try {
    // converts string numbers,null,objects 
    // ... throws on strings that can't be converted
    return JSON.parse(value);
  } catch(error) {
    return value;
  }
}

Это основано на том факте, что JSON.parse при вызове строки, содержащей строку в кавычках, вернет строку в кавычках, то есть JSON.parse(“\"a\"") returns “a". Приведенный ниже тест будет печатать true,

console.log(Object.keys(test).every(key=>fromKey(key)===test[key]));

Строка if(value==="undefined") return обрабатывает особый случай, когда toKey генерирует значение, которое не может быть проанализировано, но не должно возвращаться в виде строки.

Обратите внимание: бывают ситуации, когда приведенный выше код не работает, например попытка использовать функцию в качестве ключа. Кроме того, преобразование объектов JavaScript в строки для использования их в качестве ключей может быть весьма неэффективным с точки зрения скорости и создавать чудовищные ключи, поглощающие оперативную память. Тем не менее, статья предназначена для иллюстрации, а не для предоставления комплексного подхода к генерации ключей для объектов. В качестве упражнения вы можете изменить toKey, чтобы обрабатывать объекты и поля идентификатора поиска или генерировать ключи, используя что-то вроде uuid.

Краткое прочтение, но надеюсь полезно!