Привет, друзья!
Radash — это современная альтернатива Lodash, библиотека, предоставляющая набор часто используемых утилит (вспомогательных функций), реализованных на TypeScript. В данной статье мы вместе с вами разберем исходный код нескольких наиболее интересных утилит.
Репозиторий с кодом библиотеки находится здесь.
Обратите внимание: я позволил себе немного модифицировать отдельные утилиты для повышения читаемости и сокращения шаблонного кода. Также в нескольких местах пришлось поправить типы.
Для тех, кому интересно, вот большая коллекция сниппетов JavaScript.
Начнем с чего-нибудь попроще.
Генерации и извлечение произвольных значений
Функция для генерации произвольного целого числа в заданном диапазоне
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1) + min);
Пример использования:
const randomInt = randomInt(1, 10);
console.log(randomInt); // 6
Классика.
Функция для извлечения произвольного элемента из массива
const draw = <T>(arr: T[]): T | null => {
// длина массива
const len = arr.length;
// если массив является пустым
if (len === 0) {
return null;
}
// генерируем произвольное целое число от первого до последнего индекса массива
const i = random(0, len - 1);
// возвращаем произвольный элемент
return arr[i];
};
Пример использования:
const arr = [1, 2, 3, 4, 5];
const randomItem = draw(arr);
console.log(randomItem); // 4
Если требуется возвращать только уникальные элементы, можно мутировать исходный массив следующим образом:
const draw = <T>(arr: T[], mutate?: boolean): T | null => {
const len = arr.length;
if (len === 0) {
return null;
}
const i = random(0, len - 1);
// метод `splice` мутирует исходный массив и возвращает массив извлеченных элементов
return mutate ? arr.splice(i, 1)[0] : arr[i];
};
Пример использования:
const arr = [1, 2, 3, 4, 5];
const randomItems = [];
while (arr.length) {
const randomItem = draw(arr, true);
randomItems.push(randomItem);
}
// получается своего рода перемешивание элементов исходного массива
console.log(randomItems); // [2, 5, 1, 4, 3]
Это приводит нас к следующей функции.
Функция для перемешивания элементов массива
export const shuffle = <T>(arr: T[]): T[] => {
return arr
// преобразуем исходный массив в массив объектов со свойствами `random` и `value`
.map((a) => ({ random: Math.random(), value: a }))
// сортируем массив по полю `random`
.sort((a, b) => a.random - b.random)
// возвращаем оригинальные значения
.map((a) => a.value);
};
Пример использования:
const arr = [1, 2, 3, 4, 5];
const randomItems = shuffle(arr);
console.log(randomItems); // [4, 2, 5, 1, 3]
Простейшая, но не очень правильная реализация такой функции выглядит следующим образом:
const shuffle = <T>(arr: T[]): T[] =>
arr.slice().sort(() => Math.random() - 0.5);
Более правильный вариант — Тасование Фишера-Йетса:
const shuffle = <T>(arr: T[]): T[] => {
let len = arr.length;
while (len) {
const i = ~~(Math.random() * len--);
[arr[len], arr[i]] = [arr[i], arr[len]];
}
return arr;
};
Функция для генерации произвольной строки заданной длины
export const uid = (length: number, symbols: string = "") => {
// символы, используемые для генерации строки
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + symbols;
// результат
let _uid = "";
for (let i = 1; i <= length; i++) {
// извлекаем случайный символ
const i = random(0, chars.length - 1);
// и добавляем его к результату
_uid += characters[i];
}
// возвращаем результат
return _uid;
};
Пример использования:
const randomStr = uid(10);
console.log(randomStr); // xQZc1hzSqa
Простейшая реализация такой функции выглядит следующим образом:
// 10-11 символов
// преобразуем число в строку и удаляем первые 2 символа - `0.`
const uid = () => Math.random().toString(36).slice(2);
Обратите внимание: если значения, генерируемые такими функциями, планируется использовать в качестве идентификаторов DOM-элементов
, то следует помнить, что id
элемента не может начинаться с числа. Возможно, это как-то связано с тем, что такие элементы становятся свойствами глобального объекта window
. Для решения данной задачи достаточно заменить первое число в строке на какую-нибудь букву, например, x
:
// заменяем первое число буквой `x`
const uid = () => Math.random().toString(36).slice(2).replace(/\d/, "x");
Двигаемся дальше.
Работа с массивами и объектами
Функция-генератор для формирования диапазона целых чисел
function* range(
// начало диапазона
start: number,
// конец диапазона
end: number,
// шаг
step: number = 1
): Generator<number> {
for (let i = start; i <= end; i += step) {
yield i;
// останавливаем генератор, если текущее значение плюс шаг больше конца диапазона
if (i + step > end) break;
}
}
Пример использования:
const numsRange = range(1, 10, 2);
console.log(numsRange.next().value); // 1
console.log(numsRange.next().value); // 3
console.log(...numsRange); // 5 7 9
Функция для генерации массива с диапазоном целых чисел
// функция возвращает массив из генератора
const list = (start: number, end: number, step: number = 1): number[] =>
Array.from(range(start, end, step));
Пример использования:
const arrWithNumsRange = list(1, 10, 2);
console.log(arrWithNumsRange); // [1, 3, 5, 7, 9]
Для генерации массива с числами двойной точности можно воспользоваться следующей функцией:
const list = (
start: number = 0,
stop: number = 1,
step: number = 0.1,
// точность округления
precision: number = 1
) =>
Array.from({ length: (stop - start) / step + 1 }, (_, i) =>
// метод `toFixed` возвращает строку
// конвертируем ее в число
Number((start + i * step).toFixed(precision))
);
Пример использования:
const arrWithNumsRange = list();
console.log(arrWithNumsRange);
// [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
Функция для разделение массива на части
const chunk = <T>(arr: T[], size: number = 2): T[][] => {
// определяем количество частей посредством деления длины массива
// на указанный размер с округлением в большую сторону
const chunks = Math.ceil(arr.length / size);
// создаем новый массив с длиной, равной количеству частей
// перебираем его элементы и возвращаем копии частей исходного массива указанного размера
return Array.from({ length: chunks }, (_, i) =>
arr.slice(i * size, i * size + size)
);
};
Пример использования:
const arr = [1, 2, 3, 4, 5];
const chunked = chunk(arr, 2);
console.log(chunked); // [ [1, 2], [3, 4], [5] ]
Функция для преобразования массива в объект
const objectify = <T, Key extends string | number | symbol, Value = T>(
arr: T[],
// геттер ключей
getKey: (i: T) => Key,
// геттер значений, по умолчанию возвращающий элемент массива - объект
getValue: (i: T) => Value = (i) => i as unknown as Value
): Record<Key, Value> =>
arr.reduce(
// возвращается объект
(acc, item) => ({
...acc,
// динамическое свойство
[getKey(item)]: getValue(item),
}),
{} as Record<Key, Value>
);
Пример использования:
const usersArr = [
{ name: "Alice", age: 23 },
{ name: "Bob", age: 32 },
];
// геттер ключей
const usersObj = objectify(usersArr, (u) => u.name);
console.log(usersObj);
/*
{
Alice: { name: 'Alice', age: 23 },
Bob: { name: 'Bob', age: 32 }
}
*/
// геттеры ключей и значений
const usersObj2 = objectify(
usersArr,
(u) => u.name,
(u) => u.age
);
console.log(usersObj2); // { Alice: 23, Bob: 32 }
Функция для преобразования объекта в массив
const listify = <TVal, TKey extends string | number | symbol, KRes>(
obj: Record<TKey, TVal>,
// функция-преобразователь
toItem: (key: TKey, val: TVal) => KRes
) => {
const entries = Object.entries(obj);
if (entries.length === 0) return [];
return entries.reduce((acc, entry) => {
return [...acc, toItem(entry[0] as TKey, entry[1] as TVal)];
}, [] as KRes[]);
};
Пример использования:
const usersObj = {
alice: {
age: 23,
},
bob: {
age: 32,
},
};
// `key` - ключ/имя пользователя в нижнем регистре
// `val` - объект пользователя: `{ age: number }`
const usersArr = listify(usersObj, (key, val) => ({
// разворачиваем объект
...val,
// "капитализируем" имя
name: key[0].toUpperCase() + key.slice(1),
}));
console.log(usersArr);
/*
[
{ age: 23, name: "Alice" },
{ age: 32, name: "Bob" }
]
*/
Теперь кое-что более интересное.
Работа с функциями
Частичное применение функции
const partial =
// (функция, основные параметры)
(fn: Function, ...args: any[]) =>
// (дополнительные параметры)
(...rest: any[]) =>
fn(...args, ...rest);
Пример использования:
// скидка
const discount = 0.1;
// функция для получение цены со скидкой, равной 10% стоимости товара
const getPriceWithDiscount = partial(
// функция для получения цены со скидкой
(d: number, p: number) => p - p * d,
// скидка
discount
);
// цена
const price = 100;
// цена со скидкой
const priceWithDiscount = getPriceWithDiscount(price);
console.log(priceWithDiscount); // 90
Функция для проксирования свойств объекта
Данная функция позволяет выполнять определенные операции при доступе к свойству объекта (реализовано с помощью объекта Proxy):
export const proxied = <T, K>(
// обработчик, вызываемый при доступе к свойству
handler: (prop: T) => K
): Record<string, K> =>
new Proxy(
{},
{
get: (_, prop: any) => handler(prop),
}
);
Пример использования:
const person = {
firstName: "Harry",
lastName: "Heman",
};
const proxiedPerson = proxied((prop: keyof typeof person) =>
// переводим значение свойства в верхний регистр
person[prop].toUpperCase()
);
console.log(proxiedPerson.firstName); // HARRY
Функция мемоизации
Данная функция позволяет мемоизировать (memoize) результаты вызова другой функции:
// тип функции, передаваемой в качестве параметра функции мемоизации
type Func<TArgs = any, KReturn = any | void> = (...args: TArgs[]) => KReturn;
// тип кеша - объект со свойствами `exp` и `value`
type Cache<T> = Record<string, { exp: number; value: T }>;
// функция кеширования
const memoize = <T>(
// кеш - объект
cache: Cache<T>,
// кешируемая функция
fn: Func<any, T>,
// геттер ключа для доступа к кешу
keyFunc: Func<string> | null,
// время жизни кеша - срок, в течение которого кеш считается валидным
ttl: number
) => {
return function callWithMemo(...args: any): T {
// ключ для доступа к кешу
const key = keyFunc ? keyFunc(...args) : JSON.stringify({ args });
// имеется ли значения в кеше?
const existing = cache[key];
// если имеется
if (existing !== undefined) {
// и время жизни кеша не истекло
if (existing.exp > new Date().getTime()) {
// возвращаем значение
return existing.value;
}
}
// вычисляем значение
const result = fn(...args);
// записываем его в кеш
cache[key] = {
exp: new Date().getTime() + ttl,
value: result,
};
// возвращаем значение
return result;
};
};
const memo = <TFunc extends Function>(
// кешируемая функция
fn: TFunc,
// настройки
{
// геттер ключа для доступа к кешу
key = null,
// время жизни кеша
ttl = 300,
}: {
key?: Func<any, string> | null;
ttl?: number;
} = {}
) => memoize({}, fn as any, key, ttl) as any as TFunc;
Пример использования:
const factorial = (n: number): number => (n <= 1 ? 1 : n * factorial(n - 1));
const memoizedFactorial = memo(factorial);
console.time("t1");
// первый вызов мемоизированной функции - значение вычисляется
memoizedFactorial(150);
console.timeEnd("t1"); // 0.10...
console.time("t2");
// второй вызов мемоизированной функции с тем же аргументом - значение доставляется из кеша
memoizedFactorial(150);
console.timeEnd("t2"); // 0.01...
Дебаунсинг и троттлинг
Простыми словами: дебаунсинг (debouncing) — это когда функция выполняется один раз по истечении указанного времени с момента последнего вызова, независимо от количества ее вызовов, а троттлинг (throttling) — это когда в течение определенного времени функция выполняется только один раз, несмотря на количество ее вызовов (обычно функция выполняется в начале указанного периода).
Начнем с дебаунсинга:
// функция принимает коллбек, вызываемый по истечении указанного времени,
// и задержку в мс
export const debounce = (fn: Function, ms: number) => {
let timer: any = null;
const debounced = (...args: any[]) => {
// очищаем таймер при каждом вызове функции
clearTimeout(timer);
timer = setTimeout(() => {
// выполняем коллбек
fn(...args);
// очищаем таймер
clearTimeout(timer);
}, ms);
};
return debounced;
};
Пример использования:
<p id="par">0</p>
<button id="btn">click</button>
// получаем ссылку на параграф
const par = document.getElementById("par");
// счетчик
let clicks = 0;
// обработчик клика
const onClick = () => {
// увеличиваем значение счетчика
clicks += 1;
// выводим значение счетчика в качестве текста параграфа
(par as HTMLParagraphElement).textContent = clicks.toString();
};
// дебаунсинг
const debouncedOnClick = debounce(onClick, 1000);
// получаем ссылку на кнопку
const btn = document.getElementById("btn");
// регистрируем обработчик
(btn as HTMLButtonElement).onclick = throttledOnClick;
Сколько бы раз мы не нажали кнопку, значение счетчика увеличится только на 1 по истечении 1 сек с момента последнего нажатия. Как правило, дебаунсинг применяется в отношении обработчиков таких событий, как scroll
и mousemove
(или touchmove
).
Троттлинг:
// функция принимает коллбек, вызываемый один раз в течение указанного времени,
// и интервал в мс
export const throttle = (fn: Function, ms: number) => {
// индикатор готовности
let ready = true;
const throttled = (...args: any[]) => {
// если индикатор готовности имеет значение `false`
if (!ready) return;
// выполняем коллбек
fn(...args);
// обновляем индикатор
ready = false;
const timer = setTimeout(() => {
// обновляем индикатор по истечении указанного времени
ready = true;
// очищаем таймер
clearTimeout(timer);
}, ms);
};
return throttled;
};
Пример использования:
// перепишем последний пример
const throttledOnClick = throttle(onClick, 1000);
const btn = document.getElementById("btn");
(btn as HTMLButtonElement).onclick = throttledOnClick;
Теперь сколько бы раз мы не нажимали кнопку, значение счетчика будет увеличиваться на 1 не чаще одного раза в сек. Троттлинг может применяться в отношении обработчиков таких событий, как keydown
или mousedown
.
Напоследок самое интересное.
Работа с асинхронными функциями
Функция для выполнения асинхронной функции
// тип аргументов
type ArgumentsType<T> = T extends (...args: infer U) => any ? U : never;
// тип результата выполнения промиса
type UnwrapPromisify<T> = T extends Promise<infer U> ? U : T;
// функция возвращает [ ошибка, результат ]
// ошибка и результат могут иметь значение `null`
export const tryit = <TFunction extends (...args: any) => any>(
fn: TFunction
) => {
return async (
...args: ArgumentsType<TFunction>
): Promise<[Error | null, UnwrapPromisify<ReturnType<TFunction>> | null]> => {
try {
return [null, await fn(...(args as any))];
} catch (err) {
return [err as any, null];
}
};
};
Пример использования:
const getUsers = tryit(() =>
fetch("https://jsonplaceholder.typicode.com/users?_limit=2").then((r) =>
r.json()
)
);
const [error, users] = await getUsers();
console.log(error); // null
console.log(users); // [ [user], [user] ]
// ошибка в урле
const getUsers2 = tryit(() =>
fetch("https://jsonplaceholder.typicod.com/users?_limit=2").then((r) =>
r.json()
)
);
const [error2, users2] = await getUsers2();
console.log(error2?.message); // Failed to fetch
console.log(users2); // null
Функция для повторного выполнения асинхронной операции
Данная функция позволяет предпринимать несколько попыток выполнения асинхронной операции:
export const retry = async <TResponse>(
// выполняемая операция - промис
fn: (exit: (err: any) => void) => Promise<TResponse>,
options: {
// количество попыток
times?: number;
// задержка между попытками
delay?: number | null;
// экспоненциальная задержка
backoff?: (count: number) => number;
}
): Promise<TResponse | void> => {
// по умолчанию предпринимается 3 попытки
const times = options?.times ?? 3;
const delay = options?.delay;
const backoff = options?.backoff ?? null;
for (const i of list(1, times)) {
const [err, result] = (await tryit(fn)((err: any) => {
throw { _exited: err };
})) as [any, TResponse];
// если ошибки нет, возвращаем результат
if (!err) return result;
// если возникла ошибка, выбрасываем ее
if (err._exited) throw err._exited;
// если количество попыток исчерпано, выбрасываем исключение
if (i === times) throw err;
// задержка между попытками
if (delay) await sleep(delay);
// экспоненциальная задержка
if (backoff) await sleep(backoff(i));
}
};
Пример использования:
// ошибка в урле
const getUsers = () =>
fetch("https://jsonplaceholder.typicod.com/users?_limit=2").then((r) =>
r.json()
);
await retry(getUsers, { delay: 1000 });
// после 3 попыток с задержкой в 1 сек выбрасывается исключение `Uncaught TypeError: Failed to fetch`
Функция для одновременного выполнения нескольких асинхронных операций
Данная функция позволяет выполнять несколько асинхронных операций за один раз (реализовано с помощью Promise.all()):
// тип результата выполнения асинхронной операции
type WorkItemResult<K> = {
index: number;
result: K;
error: any;
};
// класс кастомной ошибки
class AggregateError extends Error {
errors: Error[];
constructor(errors: Error[]) {
super();
this.errors = errors;
}
}
// вспомогательная функция сортировки
const sort = <T>(
arr: T[],
getter: (item: T) => number,
desc = false
) => {
if (!arr) return [];
const asc = (a: T, b: T) => getter(a) - getter(b);
const dsc = (a: T, b: T) => getter(b) - getter(a);
return arr.slice().sort(desc === true ? dsc : asc);
};
// вспомогательная функция разделения массива пополам
// в зависимости от логического значения, возвращаемого переданной функцией `condition`
const fork = <T>(
arr: T[],
condition: (item: T) => boolean
): [T[], T[]] => {
if (!arr) return [[], []];
return arr.reduce(
(acc, item) => {
const [a, b] = acc;
if (condition(item)) {
return [[...a, item], b];
} else {
return [a, [...b, item]];
}
},
[[], []] as [T[], T[]]
);
};
// основная функция
const parallel = async <T, K>(
// количество одновременно выполняемых асинхронных операций
limit: number,
// массив параметров для операции
arr: T[],
// операция
fn: (item: T) => Promise<K>
): Promise<K[]> => {
// преобразуем массив параметров в массив объектов
const work = arr.map((item, index) => ({
index,
item,
}));
// обрабатываем этот массив
const processor = async (res: (value: WorkItemResult<K>[]) => void) => {
// массив результатов
const results: WorkItemResult<K>[] = [];
while (true) {
// берем последний элемент массива - метод `pop` мутирует исходный массив
const next = work.pop();
// если элементы кончились, возвращаем результат
if (!next) return res(results);
// выполняем операцию, получаем результаты
const [error, result] = await tryit(fn)(next.item);
// помещаем результаты в массив
results.push({
error,
result: result as K,
index: next.index,
});
}
};
// создаем очередь
const queues = list(1, limit).map(() => new Promise(processor));
// ждем завершения всех операций
const itemResults = (await Promise.all(queues)) as WorkItemResult<K>[][];
// сортируем массив результатов по индексам
// и делим его по наличию ошибок
const [errors, results] = fork(
sort(itemResults.flat(), (r) => r.index),
(x) => !!x.error
);
// если имеются ошибки
if (errors.length > 0) {
// выбрасываем кастомное исключение
throw new AggregateError(errors.map((error) => error.error));
}
// иначе возвращаем массив результатов
return results.map((r) => r.result);
};
Пример использования:
// массив путей
const urls = [
"https://jsonplaceholder.typicode.com/users/1",
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/todos/1",
];
// функция для отправки запроса по указанному урлу
const fetcher = (url: string) => fetch(url).then((r) => r.json());
// данные
const data = await parallel(3, urls, async (url) => await fetcher(url));
console.log(data); // [ [user], [post], [todo] ]
const urls2 = [
"https://jsonplaceholder.typicode.com/users/1",
// ошибка в урле
"https://jsonplaceholder.typicod.com/posts/1",
"https://jsonplaceholder.typicode.com/todos/1",
];
const [err, data2] = await tryit(parallel)(
3,
urls2,
// не хватает типа
async (url) => await fetcher(url as string)
);
console.log(data2); // null
// не хватает типа
console.log((err as AggregateError).errors); // [TypeError: Failed to fetch...]
console.log((err as AggregateError).errors[0].message); // Failed to fetch
Пожалуй, это все, чем я хотел поделиться с вами в этой статье. Мы рассмотрели примерно половину утилит, предоставляемых Radash
, остальные функции показались мне не такими интересными. Надеюсь, вы нашли для себя что-то полезное и не зря потратили время.
Благодарю за внимание и happy coding!
Комментарии (14)
printf
07.09.2022 14:26+1const randomInt = (min: number, max: number) => ~~(Math.random() * (max - min + 1) + min)
Я эту функцию, буквально эту самую, даю соискателям на собеседовании и прошу найти в ней ошибку.
Грубых ошибок в ней две:
Распределение не униформное, см. как правильно.
Результат обрезан до int32, и получаются причудливые результаты вида
randomInt(8500000000, 9000000000) // 40568060
Уверен, что это просто случайность, и остальные функции в библиотеке не скопированы из самого глупого ответа на StackOverflow.
aio350 Автор
07.09.2022 19:05"Распределение не униформное..." - покажите реализацию на TypeScript?
"Результат обрезан до int32..." - это я погорячился, в библиотеке вместо~~
используетсяMath.floor()
, поправил.
fransua
07.09.2022 15:35+1Наверное в Objectify лучше заменить reduce на
Object.fromEntries()
:function *getEntries(arr, getKey, getValue){ for (let x of arr) yield [getKey(x), getValue(x)] } export function objectify<T, U>(arr: T[], getKey: (x: T) => string, getValue: (x: T) => U) { return Object.fromEntries(getEntries(arr, getKey, getValue)) }
aio350 Автор
07.09.2022 19:06В чем преимущества?
fransua
07.09.2022 20:23+1В варианте предложенном автором:
создается и уничтожается O(n) объектов (это легко исправить)
возвращаемый объект в V8 хранится менее эффективным способом (это исправить сложнее)
Когнитивная сложность у reduce большая
Alexandroppolus
07.09.2022 23:44+1В listify та же проблема - reduce, который мусорит массивами. Итого, на ровном месте, N^2 по времени и памяти. Надо пушить.
corporateanon
09.09.2022 09:25Там простого
map
-а с головой хватит:const listify = <TVal, KRes>( obj: Record<string, TVal>, // функция-преобразователь toItem: (key: string, val: TVal) => KRes ) => { return Object.entries(obj).map((entry) => toItem(entry[0], entry[1]) ); };
Попутно, почему я заменил
TKey
наstring
. СSymbol
в качестве ключаObject.entries
не работает. Числовые ключи всё равно преобразует в строки.
corporateanon
09.09.2022 09:08+1Ваш вариант тоже аллоцирует n массивов из 2 элементов. Да, они не собираются в один большой массив благодаря использованию гератора. Но сами порождаемые генератором значения
[getKey(x), getValue(x)]
тоже расходуют память.Предлагаю по старинке:
function objectify<T, U>( arr: T[], getKey: (x: T) => string, getValue: (x: T) => U ) { const res:{ [k: string]: U } = {}; for (const x of arr) { res[getKey(x)] = getValue(x); } return res; }
fransua
09.09.2022 13:19Согласен, аллоцирует, но по очереди, так что нельзя говорить о затратах по памяти O(n), кроме результата.
Посмотрел примерно скорость работы, Ваш вариант быстрее в 2-10 раз, в зависимости от n.
Возможно, в каких-то супер предельных случаях мой вариант может быть быстрее, но это неважно.
И должен признать, я до конца не понимаю, что конкретно делает кодres[a] = 0
и какая у него сложность)corporateanon
09.09.2022 13:31но по очереди
Я думал над этим. Мне кажется, что все пары аллоцируются в куче, и только после прохождения полного цикла уничтожаются сборщиком мусора. Хотя, возможно, движок это как-то и оптимизирует.
Шутки ради решил проверить ваш вариант без аллокаций массивов, получилось вот что:
function *getEntries<T, U>(arr: T[], getKey: (x: T) => string, getValue: (x: T) => U) { const pair = []; for (let x of arr) { pair[0] = getKey(x); pair[1] = getValue(x); yield pair; } } function objectify<T, U>(arr: T[], getKey: (x: T) => string, getValue: (x: T) => U) { return Object.fromEntries(getEntries(arr, getKey, getValue)) }
fransua
09.09.2022 14:10Должны уничтожаться внутри цикла, вроде:
function fromEntries(iterable){ const res = {}; for (let [key, value] of iterable){ res[key] = value; } return res; }
Я надеялся на то, что эта функция реализована в с++ и
res[key] = value
не создает всякие схемы объектов, и отдает уже что-то готовое.
Можно для проверки подсунуть бесконечный итератор:function* getEntries(arr, getKey, getValue) { while (true) for (let x of arr) yield [getKey(x), getValue(x)] }
и будет бесконечный цикл без OutOfMemory (в моем NodeJS по крайней мере так)
Alexandroppolus
09.09.2022 14:31я до конца не понимаю, что конкретно делает код
res[a] = 0
Если в res ещё не было ключа с таким именем, это приводит к замене некоего "внутреннего класса" (читал что-то подобное про v8). Раньше добавление ключей в цикле считалось не слишком быстрым вариантом. Как теперь, не знаю. Возможно, стоит попробовать создать объект через arr.map+Object.fromEntries, без генераторов
fransua
09.09.2022 14:53тогда точно О(n) памяти на массив уйдет.
Замерил примерно, вариант по старинке с циклом быстрее всех, потом arr.map+fromEntries и генератор самый медленный((
return
Нет ничего хуже использования null там, где можно без них обойтись. Если у тебя всюду в языке доступ к элементу массива, которого нет, возвращает undefined, а в твоей библиотеке – null, то это диссонанс, который отпугнёт часть пользователей