Добавить в корзинуПозвонить
Найти в Дзене
ovnoCod

Язык JavaScript - Типы данных 2

Вы всё ещё используете обычные объекты для хранения пар "ключ-значение"? И массивы для проверки уникальности? Я вас понимаю. Это работает. Но работает плохо. Объекты путают строковые ключи, наследуют свойства от прототипа и не помнят порядок. А поиск в массиве - это всегда O(n), что медленно. Встречайте Map и Set - две структуры данных, которые были созданы, чтобы решить проблемы старых добрых объектов и массивов. Map - это коллекция ключ-значение, где ключом может быть что угодно: число, объект, функция, даже NaN. А Set - коллекция уникальных значений любого типа. Сегодня мы разберём, почему они лучше объектов и массивов во многих сценариях, и как использовать их силу. Map - это коллекция для хранения пар "ключ-значение", где ключи могут быть любого типа. javascript const map = new Map();
// Добавление
map.set("name", "Анна");
map.set(42, "число");
map.set(true, "булево");
map.set({ id: 1 }, "объект");
map.set(() => {}, "функция");
// Получение
console.log(map.get("name")); // "Анна
Оглавление
картинка взята с ya.ru
картинка взята с ya.ru

Два короля коллекций: Полное руководство по Map и Set в JavaScript

Вы всё ещё используете обычные объекты для хранения пар "ключ-значение"? И массивы для проверки уникальности? Я вас понимаю. Это работает. Но работает плохо. Объекты путают строковые ключи, наследуют свойства от прототипа и не помнят порядок. А поиск в массиве - это всегда O(n), что медленно.

Встречайте Map и Set - две структуры данных, которые были созданы, чтобы решить проблемы старых добрых объектов и массивов.

Map - это коллекция ключ-значение, где ключом может быть что угодно: число, объект, функция, даже NaN. А Set - коллекция уникальных значений любого типа.

Сегодня мы разберём, почему они лучше объектов и массивов во многих сценариях, и как использовать их силу.

Часть 1. Map: Объект на максималках

1.1 Что такое Map?

Map - это коллекция для хранения пар "ключ-значение", где ключи могут быть любого типа.

javascript

const map = new Map();

// Добавление
map.set("name", "Анна");
map.set(42, "число");
map.set(true, "булево");
map.set({ id: 1 }, "объект");
map.set(() => {}, "функция");

// Получение
console.log(map.get("name"));
// "Анна"
console.log(map.get(42));
// "число"
console.log(map.get(true));
// "булево"

// Проверка наличия
console.log(map.has("name"));
// true
console.log(map.has("age"));
// false

// Удаление
map.delete("name");
console.log(map.has("name"));
// false

// Размер (не .length, а .size!)
console.log(map.size);
// 4

1.2 Ключи любого типа - главное преимущество

В отличие от обычных объектов, где ключи всегда превращаются в строки, в Map ключи остаются теми, кем были.

javascript

const objMap = {};
const map = new Map();

const keyObj = { id: 1 };
const keyFunc = () => {};
const keyNum = 42;

// Объект (все ключи стали строками)
objMap[keyObj] = "значение 1";
objMap[keyFunc] = "значение 2";
objMap[keyNum] = "значение 3";
console.log(Object.keys(objMap));
// ["[object Object]", "() => {}", "42"]

// Map (ключи сохранили тип)
map.set(keyObj, "значение 1");
map.set(keyFunc, "значение 2");
map.set(keyNum, "значение 3");

console.log(map.get(keyObj));
// "значение 1"
console.log(map.get(keyFunc));
// "значение 2"
console.log(map.get(keyNum));
// "значение 3"

1.3 Map против обычного объекта: битва титанов

-2

1.4 Методы Map

javascript

const map = new Map();

// set(key, value) - добавляет или обновляет
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);

// get(key) - получает значение
console.log(map.get("b"));
// 2

// has(key) - проверяет наличие
console.log(map.has("a"));
// true

// delete(key) - удаляет
map.delete("b");

// clear() - очищает всё
map.clear();
console.log(map.size);
// 0

// size - количество элементов (не метод!)
console.log(map.size);

1.5 Итерация по Map

Map - это перебираемый объект. Он имеет три метода для итерации и сам работает с for...of.

javascript

const map = new Map([
["name", "Анна"],
["age", 25],
["city", "Москва"]
]);

// for...of (возвращает пары [key, value])
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// name: Анна
// age: 25
// city: Москва

// keys() - итератор ключей
for (const key of map.keys()) {
console.log(key);
// name, age, city
}

// values() - итератор значений
for (const value of map.values()) {
console.log(value);
// Анна, 25, Москва
}

// entries() - итератор пар (то же, что и for...of)
for (const [key, value] of map.entries()) {
console.log(key, value);
}

// forEach (обратите внимание на порядок аргументов: значение, ключ)
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});

Часть 2. Set: Коллекция уникальности

2.1 Что такое Set?

Set - это коллекция уникальных значений. Любое значение может встретиться только один раз.

javascript

const set = new Set();

// Добавление
set.add(1);
set.add(2);
set.add(3);
set.add(2);
// игнорируется, так как 2 уже есть
set.add(3);
// игнорируется

console.log(set.size);
// 3
console.log(set.has(2));
// true
console.log(set.has(5));
// false

// Удаление
set.delete(2);
console.log(set.has(2));
// false

// Очистка
set.clear();
console.log(set.size);
// 0

2.2 Уникальность работает с любыми типами

javascript

const set = new Set();

set.add(1);
set.add("1");
set.add(true);
set.add({ id: 1 });
set.add({ id: 1 });
// другой объект → добавляется!

console.log(set.size);
// 5

// NaN равен NaN в Set (в отличие от ===)
set.add(NaN);
set.add(NaN);
console.log(set.size);
// 6 (один NaN)

// Объекты сравниваются по ссылке
const obj = { id: 1 };
set.add(obj);
set.add(obj);
// не добавится (тот же объект)
console.log(set.size);
// 7 (добавился obj, но не дубликат)

2.3 Методы Set

javascript

const set = new Set([1, 2, 3, 3, 4, 4, 5]);

// add(value) - добавляет (возвращает Set для цепочек)
set.add(6).add(7).add(8);

// has(value) - проверяет наличие
console.log(set.has(5));
// true

// delete(value) - удаляет
set.delete(5);

// size - количество элементов
console.log(set.size);
// 7

// clear() - очищает
set.clear();
console.log(set.size);
// 0

2.4 Итерация по Set

javascript

const set = new Set(["a", "b", "c"]);

// for...of (значения)
for (const value of set) {
console.log(value);
// a, b, c
}

// keys() и values() - одно и то же (Set не имеет ключей)
for (const value of set.keys()) {
console.log(value);
// a, b, c
}

for (const value of set.values()) {
console.log(value);
// a, b, c
}

// entries() - возвращает пары [value, value] (для совместимости с Map)
for (const [value, sameValue] of set.entries()) {
console.log(value, sameValue);
// a a, b b, c c
}

// forEach
set.forEach((value, sameValue, set) => {
console.log(value);
});

Часть 3. Создание Map и Set из других структур

3.1 Из массива пар (Map)

javascript

// Из массива [key, value]
const map = new Map([
["name", "Анна"],
["age", 25],
["city", "Москва"]
]);

// Из обычного объекта
const obj = { name: "Анна", age: 25 };
const mapFromObj = new Map(Object.entries(obj));
console.log(mapFromObj.get("name"));
// "Анна"

3.2 Из массива (Set)

javascript

// Из массива (дубликаты удаляются)
const set = new Set([1, 2, 2, 3, 3, 4]);
console.log(set);
// Set {1, 2, 3, 4}

// Из строки
const charSet = new Set("hello");
console.log(charSet);
// Set {'h', 'e', 'l', 'o'}

Часть 4. Преобразование обратно

4.1 Map → массив

javascript

const map = new Map([["a", 1], ["b", 2], ["c", 3]]);

// В массив пар
const entries = [...map];
console.log(entries);
// [["a", 1], ["b", 2], ["c", 3]]

// Только ключи
const keys = [...map.keys()];
console.log(keys);
// ["a", "b", "c"]

// Только значения
const values = [...map.values()];
console.log(values);
// [1, 2, 3]

4.2 Map → объект

javascript

const map = new Map([["name", "Анна"], ["age", 25]]);
const obj = Object.fromEntries(map);
console.log(obj);
// { name: "Анна", age: 25 }

4.3 Set → массив

javascript

const set = new Set([1, 2, 3, 4]);
const arr = [...set];
console.log(arr);
// [1, 2, 3, 4]

// Или Array.from
const arr2 = Array.from(set);

Часть 5. Реальные кейсы

5.1 Удаление дубликатов из массива

javascript

const numbers = [1, 2, 2, 3, 3, 4, 5, 5];
const unique = [...new Set(numbers)];
console.log(unique);
// [1, 2, 3, 4, 5]

5.2 Кэш с ограничением (LRU на Map)

Map помнит порядок вставки, что идеально для реализации LRU-кэша.

javascript

class LRUCache {
constructor(limit = 5) {
this.limit = limit;
this.cache = new Map();
}

get(key) {
if (!this.cache.has(key)) return undefined;

// Обновляем порядок — удаляем и добавляем заново
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}

set(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.limit) {
// Удаляем самый старый (первый элемент)
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}

this.cache.set(key, value);
}

get size() {
return this.cache.size;
}
}

const cache = new LRUCache(3);
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
cache.set("d", 4);
// вытесняет "a"
console.log(cache.get("b"));
// 2
console.log(cache.get("a"));
// undefined

5.3 Map как счётчик

javascript

const words = ["apple", "banana", "apple", "orange", "banana", "apple"];
const count = new Map();

for (const word of words) {
count.set(word, (count.get(word) || 0) + 1);
}

console.log(count.get("apple"));
// 3
console.log(count.get("banana"));
// 2
console.log(count.get("orange"));
// 1

5.4 Map для хранения состояния (лучше объекта)

javascript

// Плохо: объект с числовыми ключами
const state = {};
state[1] = "один";
state["1"] = "один (строка)";
// перезапишет!
console.log(state);
// { "1": "один (строка)" }

// Хорошо: Map различает 1 и "1"
const mapState = new Map();
mapState.set(1, "один");
mapState.set("1", "один (строка)");
console.log(mapState.get(1));
// "один"
console.log(mapState.get("1"));
// "один (строка)"

5.5 Обратный Map (значение → ключ)

javascript

const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
const reverse = new Map([...map].map(([k, v]) => [v, k]));

console.log(reverse.get(2));
// "b"
console.log(reverse.get(1));
// "a"

5.6 Пересечение и разность множеств (Set)

javascript

const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// Объединение
const union = new Set([...setA, ...setB]);
console.log([...union]);
// [1, 2, 3, 4, 5, 6]

// Пересечение
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log([...intersection]);
// [3, 4]

// Разность (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log([...difference]);
// [1, 2]

// Симметрическая разность (A ∪ B - A ∩ B)
const symmetricDiff = new Set([
...[...setA].filter(x => !setB.has(x)),
...[...setB].filter(x => !setA.has(x))
]);
console.log([...symmetricDiff]);
// [1, 2, 5, 6]

5.7 Слайсы и группировка данных

javascript

const users = [
{ id: 1, name: "Анна", role: "admin" },
{ id: 2, name: "Борис", role: "user" },
{ id: 3, name: "Вика", role: "admin" },
{ id: 4, name: "Глеб", role: "user" }
];

// Группировка по роли с помощью Map
const byRole = new Map();
for (const user of users) {
if (!byRole.has(user.role)) {
byRole.set(user.role, []);
}
byRole.get(user.role).push(user);
}

console.log(byRole.get("admin"));
// [{ id: 1, name: "Анна", ... }, { id: 3, ... }]
console.log(byRole.get("user"));
// [{ id: 2, ... }, { id: 4, ... }]

// Более элегантно с reduce
const byRoleReduce = users.reduce((map, user) => {
return map.set(user.role, [...(map.get(user.role) || []), user]);
}, new Map());

Часть 6. Map vs Object: Когда что использовать?

Используйте Map, когда:

  • Ключи не строки - числа, объекты, функции, символы
  • Частые операции добавления/удаления - Map оптимизирован для этого
  • Важен порядок - Map сохраняет порядок вставки
  • Нужен простой размер - .size удобнее Object.keys().length
  • Итерация - Map перебирается проще и предсказуемее

Используйте Object, когда:

  • Ключи только строки - обычные объекты проще
  • Нужна сериализация в JSON - JSON.stringify() работает с объектами
  • Вы работаете с API - большинство API возвращают объекты
  • Требуется прототипное наследование - хотя это редкость
  • Производительность при чтении - в некоторых движках объекты чуть быстрее

Часть 7. WeakMap и WeakSet: Сборщик мусора в помощь

WeakMap и WeakSet - это "слабые" версии коллекций. Их ключи/элементы - только объекты, и они не предотвращают сборку мусора.

7.1 WeakMap

javascript

const weakMap = new WeakMap();

let obj = { id: 1 };
weakMap.set(obj, "данные для объекта");

console.log(weakMap.get(obj));
// "данные для объекта"

// Когда obj станет недостижим, он будет удалён из weakMap автоматически
obj = null;
// Через некоторое время сборщик мусора удалит и obj, и запись в weakMap

// WeakMap нельзя итерировать
console.log(weakMap.size);
// undefined (нет свойства size)
// weakMap.keys() // Ошибка!

7.2 WeakSet

javascript

const weakSet = new WeakSet();

let user1 = { name: "Анна" };
let user2 = { name: "Борис" };

weakSet.add(user1);
weakSet.add(user2);

console.log(weakSet.has(user1));
// true

user1 = null;
// теперь user1 может быть собран сборщиком

// weakSet нельзя итерировать
// weakSet.size не существует

7.3 Классическое применение WeakMap: приватные данные

javascript

const privateData = new WeakMap();

class User {
constructor(name, password) {
// Приватные данные хранятся в WeakMap
privateData.set(this, { password });
this.name = name;
}

checkPassword(password) {
const data = privateData.get(this);
return data && data.password === password;
}
}

const user = new User("Анна", "secret123");
console.log(user.name);
// "Анна"
console.log(user.password);
// undefined (приватно!)
console.log(user.checkPassword("secret123"));
// true
console.log(user.checkPassword("wrong"));
// false

// Когда user будет удалён, запись в privateData исчезнет автоматически

Часть 8. Производительность: Map/Set vs Object/Array

javascript

// Тест производительности (концептуально)
const iterations = 1000000;

// Object vs Map (запись)
console.time("Object set");
const obj = {};
for (let i = 0; i < iterations; i++) {
obj[i] = i;
}
console.timeEnd("Object set");
// ~30ms

console.time("Map set");
const map = new Map();
for (let i = 0; i < iterations; i++) {
map.set(i, i);
}
console.timeEnd("Map set");
// ~35ms (немного медленнее)

// Map vs Object (чтение)
console.time("Object get");
for (let i = 0; i < iterations; i++) {
const _ = obj[i];
}
console.timeEnd("Object get");
// ~25ms

console.time("Map get");
for (let i = 0; i < iterations; i++) {
const _ = map.get(i);
}
console.timeEnd("Map get");
// ~28ms

// Set vs Array (уникальность)
console.time("Array includes");
const arr = [];
for (let i = 0; i < iterations; i++) {
if (!arr.includes(i)) arr.push(i);
}
console.timeEnd("Array includes");
// очень медленно (O(n²))

console.time("Set has");
const set = new Set();
for (let i = 0; i < iterations; i++) {
set.add(i);
}
console.timeEnd("Set has");
// быстро (O(1))

Вывод: Для операций поиска Set.has() и Map.get() значительно быстрее Array.includes() и Object при большом количестве данных.

Часть 9. Подводные камни

Камень #1: Map.get() не возвращает undefined, если ключа нет

javascript

const map = new Map();
console.log(map.get("missing"));
// undefined
console.log(map.get("missing") === undefined);
// true

// НО! Значение может быть undefined
map.set("key", undefined);
console.log(map.get("key"));
// undefined

// Как отличить?
console.log(map.has("key"));
// true
console.log(map.has("missing"));
// false

Камень #2: Объекты как ключи - сравнение по ссылке

javascript

const map = new Map();
const key = { id: 1 };

map.set(key, "value");
console.log(map.get({ id: 1 }));
// undefined (разные объекты!)
console.log(map.get(key));
// "value" (тот же объект)

Камень #3: Set не дедуплицирует объекты

javascript

const set = new Set();
set.add({ id: 1 });
set.add({ id: 1 });
console.log(set.size);
// 2 (разные объекты!)

Итог: Манифест Map и Set

  1. Map - коллекция ключ-значение с ключами любого типа, сохраняет порядок.
  2. Set - коллекция уникальных значений любого типа.
  3. Ключи в Map - не приводятся к строкам (в отличие от объектов).
  4. Размер - .size, не .length.
  5. Итерация - for...of, keys(), values(), entries(), forEach.
  6. Удаление дубликатов из массива - [...new Set(array)].
  7. WeakMap / WeakSet - для данных, которые должны умереть вместе с объектом.
  8. Производительность - Map и Set быстрее для частых операций поиска.

Финальный тест (проверьте себя):

javascript

const map = new Map();
map.set(1, "число");
map.set("1", "строка");
map.set(true, "булево");

console.log(map.get(1));
console.log(map.get("1"));
console.log(map.size);

const set = new Set([1, 2, 2, 3, 3, 4]);
console.log([...set]);

const obj = { a: 1 };
map.set(obj, "объект");
console.log(map.get({ a: 1 }));
console.log(map.get(obj));

Ответы: "число", "строка", 3, [1, 2, 3, 4], undefined (разный объект), "объект".

Map и Set - это не замена объектам и массивам, а дополнение. Они решают конкретные проблемы: уникальность, ключи-не-строки, порядок, производительность поиска. Используйте их, когда они подходят, и ваш код станет чище, быстрее и надёжнее. А когда не подходят - объекты и массивы всегда рядом. Выбирайте правильный инструмент для правильной задачи.

------------------------------------------------------------------------------------------

------------------------------------------------------------------------------------------

Теневые коллекции: Почему WeakMap и WeakSet - секретное оружие JavaScript

Вы знаете Map и Set. Полезные, мощные, незаменимые. Но у них есть одна "проблема" - они не дают объектам умереть. Пока объект находится в Map или Set, сборщик мусора не тронет его, даже если больше нет других ссылок. Это хорошо, когда вы хотите сохранить данные. Но что, если вы хотите прикрепить к объекту временную информацию, которая должна исчезнуть вместе с объектом?

Здесь на сцену выходят WeakMap и WeakSet - "слабые" коллекции, которые не мешают сборщику мусора. Они - как тень объекта: существуют, пока существует объект, и исчезают вместе с ним.

Часть 1. Проблема, которую решают Weak-коллекции

Представьте, что вы пишете систему кэширования данных о пользователях. Пользователи приходят и уходят, и вы хотите хранить дополнительные данные только пока пользователь активен.

javascript

// Обычный Map - память течёт!
const userData = new Map();

function processUser(user) {
if (!userData.has(user)) {
userData.set(user, { lastAccess: Date.now(), cache: new Array(1000000) });
}
return userData.get(user);
}

let user = { name: "Анна" };
processUser(user);
// Добавляем данные в Map
user = null;
// Пользователь больше не нужен

// Но объект user всё ещё жив в userData!
// Map хранит сильную ссылку → утечка памяти

Проблема: Map держит сильную ссылку на объект-ключ. Пока Map существует, объект не будет удалён сборщиком мусора, даже если вы больше не используете его.

Решение: WeakMap. Он хранит слабые ссылки на ключи. Если объект-ключ больше нигде не используется, он может быть удалён сборщиком мусора, и запись в WeakMap исчезнет автоматически.

Часть 2. WeakMap: Теневой хранитель

2.1 Основы WeakMap

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

javascript

const weakMap = new WeakMap();

let obj = { id: 1 };
let obj2 = { id: 2 };

weakMap.set(obj, "данные для obj");
weakMap.set(obj2, "данные для obj2");

console.log(weakMap.get(obj));
// "данные для obj"
console.log(weakMap.has(obj));
// true

obj = null;
// Убираем сильную ссылку

// Теперь объект { id: 1 } может быть собран сборщиком мусора
// Когда это произойдёт, запись в weakMap исчезнет автоматически

// Через некоторое время (после GC):
console.log(weakMap.get(obj));
// undefined (obj = null)
// weakMap.get(originalObj) - уже не получить, объект удалён

2.2 Отличия WeakMap от Map

-3

2.3 Доступные методы WeakMap

javascript

const weakMap = new WeakMap();

// set(key, value) - добавить (ключ только объект!)
weakMap.set({ id: 1 }, "данные");

// get(key) - получить
const obj = { id: 2 };
weakMap.set(obj, "hello");
console.log(weakMap.get(obj));
// "hello"

// has(key) - проверить наличие
console.log(weakMap.has(obj));
// true

// delete(key) - удалить
weakMap.delete(obj);
console.log(weakMap.has(obj));
// false

// НЕТ: size, clear(), forEach, keys(), values(), entries(), for...of

Часть 3. WeakSet: Теневая коллекция уникальности

WeakSet - это коллекция уникальных объектов, где ссылки на объекты - слабые.

javascript

const weakSet = new WeakSet();

let user1 = { name: "Анна" };
let user2 = { name: "Борис" };

weakSet.add(user1);
weakSet.add(user2);

console.log(weakSet.has(user1));
// true
console.log(weakSet.has(user2));
// true

user1 = null;
// Объект { name: "Анна" } больше не используется

// Запись в weakSet исчезнет после сборки мусора

Отличия WeakSet от Set

-4

Часть 4. Зачем нужны Weak-коллекции? (Классические применения)

4.1 Приватные данные в классах

Это самое популярное применение. Вместо того чтобы хранить приватные данные в свойствах объекта (которые видны), используем WeakMap.

javascript

const privateData = new WeakMap();

class User {
constructor(name, password) {
// Приватные данные не видны снаружи
privateData.set(this, { password, createdAt: Date.now() });
this.name = name;
}

checkPassword(password) {
const data = privateData.get(this);
return data && data.password === password;
}

getAccountAge() {
const data = privateData.get(this);
return data ? (Date.now() - data.createdAt) / 1000 : 0;
}
}

const user = new User("Анна", "secret123");
console.log(user.name);
// "Анна"
console.log(user.password);
// undefined
console.log(user.createdAt);
// undefined
console.log(user.checkPassword("secret123"));
// true
console.log(user.getAccountAge());
// возраст аккаунта в секундах

// Когда user станет недостижим, privateData очистится автоматически

4.2 Кэширование с автоматической очисткой

javascript

const cache = new WeakMap();

function fetchUserData(user) {
if (!cache.has(user)) {
// Вычисляем или загружаем данные
const data = {
id: user.id,
name: user.name,
lastFetch: Date.now(),
expensiveData: new Array(1000000)
// какие-то тяжёлые данные
};
cache.set(user, data);
console.log("Данные загружены");
}
return cache.get(user);
}

let user = { id: 1, name: "Анна" };
const data1 = fetchUserData(user);
// "Данные загружены"
const data2 = fetchUserData(user);
// из кэша

user = null;
// Пользователь больше не нужен

// Когда сборщик мусора удалит объект user,
// запись в cache исчезнет автоматически.
// Нет утечки памяти!

4.3 Отслеживание посещённых объектов без утечек

javascript

const visited = new WeakSet();

function traverse(obj, callback) {
if (visited.has(obj)) return;

visited.add(obj);
callback(obj);

for (const key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
traverse(obj[key], callback);
}
}
}

const data = {
user: { name: "Анна", address: { city: "Москва" } },
meta: { version: 1 }
};

traverse(data, obj => console.log("Посещён:", obj));
// Обход без риска зацикливания и без утечек

// Когда data станет недостижимым, visited очистится

4.4 Предотвращение дублирования обработчиков

javascript

const hasListener = new WeakSet();

function addOnceListener(element, event, handler) {
if (hasListener.has(element)) return;

element.addEventListener(event, handler);
hasListener.add(element);
console.log("Обработчик добавлен");
}

const button = document.getElementById("btn");
addOnceListener(button, "click", () => console.log("Click!"));
addOnceListener(button, "click", () => console.log("Click!"));
// не добавится

// Когда кнопка удалится из DOM, запись в hasListener исчезнет

Часть 5. Почему Weak-коллекции не итерируются?

Это ключевая особенность, а не баг. Если бы WeakMap можно было итерировать, сборщик мусора не мог бы удалять объекты, потому что они были бы видны во время итерации.

javascript

const weakMap = new WeakMap();
let obj = { data: "secret" };
weakMap.set(obj, "value");

// Представьте, что итерация возможна:
// for (const [key, value] of weakMap) { ... }
// Что произойдёт, если во время итерации сборщик мусора удалит key?

// Именно поэтому WeakMap и WeakSet:
// - Не имеют методов keys(), values(), entries()
// - Не имеют forEach()
// - Не поддерживают for...of
// - Не имеют свойства size (оно было бы ненадёжным)

Часть 6. Реальные паттерны использования

6.1 Инкапсуляция DOM-связанных данных

javascript

const domData = new WeakMap();

function setElementData(element, key, value) {
if (!domData.has(element)) {
domData.set(element, {});
}
domData.get(element)[key] = value;
}

function getElementData(element, key) {
if (!domData.has(element)) return undefined;
return domData.get(element)[key];
}

const div = document.createElement("div");
setElementData(div, "timestamp", Date.now());
setElementData(div, "viewCount", 0);

console.log(getElementData(div, "timestamp"));
// время создания

div.remove();
// элемент удалён из DOM
// div = null; // если нет других ссылок, данные исчезнут

6.2 Мемоизация методов с автоматической очисткой

javascript

const memoCache = new WeakMap();

function memoize(obj, methodName, fn) {
if (!memoCache.has(obj)) {
memoCache.set(obj, new Map());
}

const methodCache = memoCache.get(obj);

return function(...args) {
const key = JSON.stringify(args);
if (methodCache.has(key)) {
console.log("Из кэша");
return methodCache.get(key);
}
const result = fn.apply(obj, args);
methodCache.set(key, result);
return result;
};
}

class ExpensiveService {
constructor(name) {
this.name = name;
}

compute(data) {
console.log("Тяжёлые вычисления...");
return data * 2;
}
}

const service = new ExpensiveService("service1");
service.compute = memoize(service, "compute", service.compute);

service.compute(5);
// "Тяжёлые вычисления..." → 10
service.compute(5);
// "Из кэша" → 10

// Когда service умрёт, кэш умрёт вместе с ним

6.3 Расширение объектов без модификации

javascript

const metadata = new WeakMap();

function extendObject(obj, extension) {
if (!metadata.has(obj)) {
metadata.set(obj, {});
}
Object.assign(metadata.get(obj), extension);
return metadata.get(obj);
}

function getExtension(obj) {
return metadata.get(obj) || {};
}

const user = { name: "Анна" };
extendObject(user, { lastLogin: Date.now(), loginCount: 1 });
extendObject(user, { loginCount: 2 });

console.log(getExtension(user));
// { lastLogin: 123..., loginCount: 2 }
console.log(user.lastLogin);
// undefined (не загрязняем объект)

Часть 7. Сравнение подходов к приватным данным

javascript

// Способ 1: Соглашение (небезопасно)
class User1 {
constructor(name) {
this._name = name;
// просто договорились не трогать
}
}
const u1 = new User1("Анна");
console.log(u1._name);
// всё видно

// Способ 2: Символы (лучше, но обходимо)
const _name = Symbol("name");
class User2 {
constructor(name) {
this[_name] = name;
}
}
const u2 = new User2("Анна");
console.log(u2[_name]);
// можно достать, если знать символ
console.log(Object.getOwnPropertySymbols(u2));
// символ виден!

// Способ 3: Закрытые поля класса (ES2022, современный стандарт)
class User3 {
#name;
// действительно приватное поле
constructor(name) {
this.#name = name;
}
}
const u3 = new User3("Анна");
// console.log(u3.#name); // SyntaxError!

// Способ 4: WeakMap (работает везде, не видно, очищается)
const privateData = new WeakMap();
class User4 {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
const u4 = new User4("Анна");
console.log(u4.getName());
// "Анна"
console.log(u4.name);
// undefined
// privateData не видна снаружи

Часть 8. Подводные камни

Камень #1: Нельзя использовать примитивы как ключи

javascript

const weakMap = new WeakMap();
weakMap.set(42, "value");
// TypeError: Invalid value used as weak map key
weakMap.set("string", "value");
// TypeError
weakMap.set(true, "value");
// TypeError

// Только объекты!
weakMap.set({}, "value");
// OK
weakMap.set([], "value");
// OK
weakMap.set(() => {}, "value");
// OK

Камень #2: Нет гарантии, когда произойдёт очистка

javascript

const weakMap = new WeakMap();
let obj = { data: "important" };
weakMap.set(obj, "secret");

obj = null;

// Мы не знаем, когда сборщик мусора запустится
// Может через миллисекунду, может через минуту
// Не полагайтесь на немедленную очистку!

Камень #3: Нельзя проверить размер

javascript

const weakMap = new WeakMap();
weakMap.set({}, 1);
weakMap.set({}, 2);
weakMap.set({}, 3);

// Нет способа узнать, сколько элементов в weakMap
// .size не существует

Часть 9. WeakMap vs Map vs Object - шпаргалка

Нужно...ИспользуйтеКлючи-примитивыMap или ObjectКлючи-объекты, данные должны жить, пока жив ключMapКлючи-объекты, данные должны умереть, когда умрёт ключWeakMapИтерируемость, размерMapПриватные данные в классахWeakMap или # (если ES2022+)Отслеживание посещённых объектов без утечекWeakSetУникальность объектов с автоматической очисткойWeakSet

Часть 10. Реальный проект: Система плагинов с WeakMap

javascript

// Каждый плагин может добавлять свои данные к объектам, не боясь конфликтов
const pluginData = new WeakMap();

class PluginManager {
registerPlugin(plugin, name) {
pluginData.set(plugin, { name, enabled: true, instances: new Set() });
}

extendObject(plugin, obj, extension) {
if (!pluginData.has(plugin)) {
throw new Error("Plugin not registered");
}

const pluginInfo = pluginData.get(plugin);
if (!pluginInfo.instances.has(obj)) {
pluginInfo.instances.add(obj);
}

if (!obj.__pluginExtensions) {
obj.__pluginExtensions = new WeakMap();
}
obj.__pluginExtensions.set(plugin, extension);
}

getExtension(plugin, obj) {
if (!obj.__pluginExtensions) return undefined;
return obj.__pluginExtensions.get(plugin);
}

getStats() {
// WeakMap нельзя итерировать, поэтому статистику не получить
console.log("Невозможно подсчитать активные объекты (и это хорошо)");
}
}

const manager = new PluginManager();
const myPlugin = { name: "Logger" };
manager.registerPlugin(myPlugin, "Logger");

const user = { name: "Анна" };
manager.extendObject(myPlugin, user, { log: () => console.log("logged") });

const extension = manager.getExtension(myPlugin, user);
extension.log();
// "logged"

// Когда user умрёт, данные плагина исчезнут

Итог: Манифест Weak-коллекций

  1. WeakMap и WeakSet хранят слабые ссылки - не мешают сборщику мусора.
  2. Ключи - только объекты - никаких примитивов.
  3. Не итерируются - нет keys(), values(), entries(), forEach, for...of.
  4. Нет .size - размер неизвестен и ненадёжен.
  5. Нет .clear() - очистка происходит автоматически.
  6. Идеальны для приватных данных - данные живут, пока жив объект.
  7. Идеальны для кэширования - автоматическая очистка без утечек.
  8. Идеальны для метаданных - не загрязняют объекты.

Финальный тест (что выведет?):

javascript

const weakMap = new WeakMap();
const obj1 = { id: 1 };
const obj2 = { id: 2 };

weakMap.set(obj1, "data1");
weakMap.set(obj2, "data2");

console.log(weakMap.get(obj1));
console.log(weakMap.has(obj2));

obj1 = null;

console.log(weakMap.get(obj1));
console.log(weakMap.get({ id: 1 }));

try {
weakMap.set(42, "number");
} catch (e) {
console.log("Ошибка!");
}

Ответ: "data1", true, undefined (после GC), undefined (другой объект), "Ошибка!" (примитив как ключ).

WeakMap и WeakSet - это инструменты для тех, кто думает о памяти. Они не нужны каждый день, но когда они нужны, они незаменимы. Если вы пишете библиотеку, фреймворк или любое приложение с долгоживущими объектами, где данные должны умирать вместе с объектами - Weak-коллекции ваши лучшие друзья. Используйте их, и ваш код будет эффективнее, а память - чище.

---------------------------------------------------------------------------------

---------------------------------------------------------------------------------

Три взгляда на объект: Как Object.keys, values и entries меняют правила игры

Вы когда-нибудь хотели перебрать объект так же легко, как массив? Пройтись по всем значениям, не думая о ключах? Или одновременно по ключам и значениям?

До появления ES6 работа с объектами была неудобной. Для перебора приходилось использовать for...in с кучей проверок на hasOwnProperty. Для получения списка свойств - писать велосипеды. Но современный JavaScript дал нам три мощных метода: Object.keys, Object.values и Object.entries.

Они превращают объекты в массивы, открывая доступ ко всем возможностям: map, filter, reduce, for...of, деструктуризация и многое другое.

Сегодня мы разберём, как использовать эти методы и почему они делают работу с объектами такой же удобной, как с массивами.

Часть 1. Проблема, которую они решают

До ES6 перебор объекта выглядел так:

javascript

const user = {
name: "Анна",
age: 25,
city: "Москва"
};

// Старый способ (проблемы с прототипом)
for (const key in user) {
if (user.hasOwnProperty(key)) {
console.log(key, user[key]);
}
}

// Получение ключей (костыль)
const keys = Object.keys ? Object.keys(user) : (function(obj) {
const result = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) result.push(key);
}
return result;
})(user);

Теперь всё просто и элегантно:

javascript

// Три современных метода
const keys = Object.keys(user);
// ["name", "age", "city"]
const values = Object.values(user);
// ["Анна", 25, "Москва"]
const entries = Object.entries(user);
// [["name", "Анна"], ["age", 25], ["city", "Москва"]]

Часть 2. Object.keys - Все ключи в одном массиве

Object.keys(obj) возвращает массив собственных перечисляемых ключей объекта.

javascript

const user = {
name: "Анна",
age: 25,
city: "Москва"
};

console.log(Object.keys(user));
// ["name", "age", "city"]

2.1 Что попадает в результат?

  • Только собственные свойства (не из прототипа)
  • Только перечисляемые (enumerable: true)
  • Только строковые ключи (символы не включаются)

javascript

const obj = Object.create({ inherited: "из прототипа" });
obj.own = "своё";
Object.defineProperty(obj, "hidden", {
value: "скрыто",
enumerable: false
});
const sym = Symbol("secret");
obj[sym] = "символ";

console.log(Object.keys(obj));
// ["own"]

2.2 Применения Object.keys

javascript

// 1. Проверка, пуст ли объект
const isEmpty = (obj) => Object.keys(obj).length === 0;
console.log(isEmpty({}));
// true
console.log(isEmpty({ a: 1 }));
// false

// 2. Подсчёт количества свойств
const count = Object.keys(user).length;
console.log(count);
// 3

// 3. Перебор с forEach
Object.keys(user).forEach(key => {
console.log(`${key}: ${user[key]}`);
});

// 4. Преобразование ключей
const upperKeys = Object.keys(user).map(key => key.toUpperCase());
console.log(upperKeys);
// ["NAME", "AGE", "CITY"]

// 5. Фильтрация ключей
const filtered = Object.keys(user).filter(key => key.startsWith('a'));
console.log(filtered);
// ["age"]

// 6. Сортировка по ключам
const sorted = Object.keys(user).sort();
console.log(sorted);
// ["age", "city", "name"]

Часть 3. Object.values - Только значения

Object.values(obj) возвращает массив значений собственных перечисляемых свойств.

javascript

const user = {
name: "Анна",
age: 25,
city: "Москва"
};

console.log(Object.values(user));
// ["Анна", 25, "Москва"]

3.1 Применения Object.values

javascript

// 1. Сумма числовых значений
const prices = { apple: 100, banana: 50, orange: 80 };
const sum = Object.values(prices).reduce((acc, price) => acc + price, 0);
console.log(sum);
// 230

// 2. Поиск максимального значения
const scores = { Анна: 95, Борис: 87, Вика: 92 };
const maxScore = Math.max(...Object.values(scores));
console.log(maxScore);
// 95

// 3. Проверка всех значений на условие
const allAdults = Object.values({ Анна: 25, Борис: 17, Вика: 30 })
.every(age => age >= 18);
console.log(allAdults);
// false

// 4. Уникальные значения
const data = { a: 1, b: 2, c: 1, d: 3 };
const unique = [...new Set(Object.values(data))];
console.log(unique);
// [1, 2, 3]

// 5. Фильтрация по значениям
const above20 = Object.values(scores).filter(score => score > 20);
console.log(above20);
// [95, 87, 92]

Часть 4. Object.entries - Ключи и значения вместе

Object.entries(obj) возвращает массив пар [key, value] для каждого собственного перечисляемого свойства.

javascript

const user = {
name: "Анна",
age: 25,
city: "Москва"
};

console.log(Object.entries(user));
// [["name", "Анна"], ["age", 25], ["city", "Москва"]]

4.1 Применения Object.entries

javascript

// 1. Удобный перебор с деструктуризацией
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}

// 2. Преобразование объекта через map
const upperValues = Object.fromEntries(
Object.entries(user).map(([key, value]) => [key, String(value).toUpperCase()])
);
console.log(upperValues);
// { name: "АННА", age: "25", city: "МОСКВА" }

// 3. Фильтрация свойств
const filtered = Object.fromEntries(
Object.entries(user).filter(([key, value]) => value !== 25)
);
console.log(filtered);
// { name: "Анна", city: "Москва" }

// 4. Обмен ключей и значений
const swapped = Object.fromEntries(
Object.entries(user).map(([key, value]) => [value, key])
);
console.log(swapped);
// { Анна: "name", 25: "age", Москва: "city" }

// 5. Работа с вложенными объектами
const nested = {
user1: { name: "Анна", age: 25 },
user2: { name: "Борис", age: 30 }
};

const allNames = Object.entries(nested).flatMap(([id, user]) =>
Object.entries(user).filter(([key]) => key === "name").map(([_, value]) => value)
);
console.log(allNames);
// ["Анна", "Борис"]

Часть 5. Object.fromEntries - Обратный путь

Object.fromEntries() делает обратное: превращает массив пар [key, value] обратно в объект.

javascript

const entries = [["name", "Анна"], ["age", 25], ["city", "Москва"]];
const obj = Object.fromEntries(entries);
console.log(obj);
// { name: "Анна", age: 25, city: "Москва" }

5.1 Применения Object.fromEntries

javascript

// 1. Преобразование Map в объект
const map = new Map([["name", "Анна"], ["age", 25]]);
const obj = Object.fromEntries(map);
console.log(obj);
// { name: "Анна", age: 25 }

// 2. Объект после цепочки преобразований
const result = Object.fromEntries(
Object.entries(user)
.filter(([_, value]) => typeof value === "string")
.map(([key, value]) => [key, value.toUpperCase()])
);
console.log(result);
// { name: "АННА", city: "МОСКВА" }

// 3. URL параметры в объект
const params = new URLSearchParams("name=Анна&age=25");
const paramsObj = Object.fromEntries(params);
console.log(paramsObj);
// { name: "Анна", age: "25" }

// 4. Обработка массивов пар
const data = [
["id", 1],
["title", "Hello"],
["active", true]
];
const item = Object.fromEntries(data);
console.log(item);
// { id: 1, title: "Hello", active: true }

Часть 6. Полный цикл: объект → массив → объект

javascript

const user = {
name: "Анна",
age: 25,
city: "Москва"
};

// 1. Преобразуем в entries
const entries = Object.entries(user);

// 2. Работаем как с массивом
const modified = entries
.filter(([key]) => key !== "age")
// убираем age
.map(([key, value]) => [key.toUpperCase(), value]);
// ключи в верхний регистр

// 3. Превращаем обратно в объект
const newUser = Object.fromEntries(modified);
console.log(newUser);
// { NAME: "Анна", CITY: "Москва" }

Часть 7. Сравнение методов

javascript

const obj = { a: 1, b: 2, c: 3 };

// Object.keys - массив ключей
console.log(Object.keys(obj));
// ["a", "b", "c"]

// Object.values - массив значений
console.log(Object.values(obj));
// [1, 2, 3]

// Object.entries - массив пар
console.log(Object.entries(obj));
// [["a", 1], ["b", 2], ["c", 3]]

-5

Часть 8. Реальные кейсы

8.1 Глубокая копия объекта (без вложенных объектов)

javascript

const original = { a: 1, b: 2, c: 3 };
const copy = Object.fromEntries(Object.entries(original));
console.log(copy);
// { a: 1, b: 2, c: 3 }
console.log(original === copy);
// false (разные объекты)

8.2 Обновление значений в объекте

javascript

const user = { name: "анна", age: 25, city: "москва" };

// Приводим все строковые значения к верхнему регистру
const upperCaseValues = Object.fromEntries(
Object.entries(user).map(([key, value]) => [
key,
typeof value === "string" ? value.toUpperCase() : value
])
);
console.log(upperCaseValues);
// { name: "АННА", age: 25, city: "МОСКВА" }

8.3 Фильтрация свойств по значению

javascript

const data = { a: 1, b: null, c: "hello", d: undefined, e: 0, f: false };

// Удаляем null, undefined, но оставляем 0 и false
const cleaned = Object.fromEntries(
Object.entries(data).filter(([_, value]) => value != null)
);
console.log(cleaned);
// { a: 1, c: "hello", e: 0, f: false }

8.4 Сравнение двух объектов

javascript

function isEqual(obj1, obj2) {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);

if (keys1.length !== keys2.length) return false;

return keys1.every(key => obj1[key] === obj2[key]);
}

console.log(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 }));
// true
console.log(isEqual({ a: 1, b: 2 }, { a: 1, c: 2 }));
// false

8.5 Выборка подмножества свойств

javascript

const user = {
id: 1,
name: "Анна",
password: "secret",
email: "anna@example.com",
age: 25
};

// Выбираем только безопасные поля
const safeUser = (({ id, name, email }) => ({ id, name, email }))(user);
console.log(safeUser);
// { id: 1, name: "Анна", email: "anna@example.com" }

// Или через Object.entries
const safeFields = ["id", "name", "email"];
const safeUser2 = Object.fromEntries(
Object.entries(user).filter(([key]) => safeFields.includes(key))
);

8.6 Группировка объектов

javascript

const users = [
{ name: "Анна", city: "Москва" },
{ name: "Борис", city: "СПб" },
{ name: "Вика", city: "Москва" },
{ name: "Глеб", city: "Казань" }
];

const grouped = users.reduce((acc, user) => {
if (!acc[user.city]) acc[user.city] = [];
acc[user.city].push(user);
return acc;
}, {});

console.log(grouped);
// {
// Москва: [{ name: "Анна", city: "Москва" }, { name: "Вика", city: "Москва" }],
// СПб: [{ name: "Борис", city: "СПб" }],
// Казань: [{ name: "Глеб", city: "Казань" }]
// }

8.7 Инвертирование объекта

javascript

const roles = { admin: "ADM", user: "USR", guest: "GST" };
const inverted = Object.fromEntries(
Object.entries(roles).map(([key, value]) => [value, key])
);
console.log(inverted);
// { ADM: "admin", USR: "user", GST: "guest" }

Часть 9. Порядок ключей

Важно знать: Object.keys, Object.values и Object.entries следуют тому же порядку, что и for...in:

  1. Сначала числовые ключи в порядке возрастания
  2. Затем строковые ключи в порядке добавления
  3. Затем символы (не включаются в эти методы)

javascript

const obj = {
100: "сто",
b: "B",
2: "два",
a: "A",
1: "один"
};

console.log(Object.keys(obj));
// ["1", "2", "100", "b", "a"]
// Числовые ключи отсортированы, строковые - по порядку добавления

Часть 10. Подводные камни

Камень #1: Символы не включаются

javascript

const sym = Symbol("secret");
const obj = { a: 1, [sym]: 2 };

console.log(Object.keys(obj));
// ["a"]
console.log(Object.values(obj));
// [1]
console.log(Object.entries(obj));
// [["a", 1]]

// Для символов используйте Object.getOwnPropertySymbols()
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols);
// [Symbol(secret)]

Камень #2: Неперечисляемые свойства игнорируются

javascript

const obj = {};
Object.defineProperty(obj, "hidden", {
value: 42,
enumerable: false
});
obj.visible = 100;

console.log(Object.keys(obj));
// ["visible"]
console.log(Object.values(obj));
// [100]
console.log(Object.entries(obj));
// [["visible", 100]]

Камень #3: Object.values и Object.entries с массивами

javascript

const arr = ["a", "b", "c"];

console.log(Object.keys(arr));
// ["0", "1", "2"] (индексы)
console.log(Object.values(arr));
// ["a", "b", "c"] (значения)
console.log(Object.entries(arr));
// [["0", "a"], ["1", "b"], ["2", "c"]]

Часть 11. Производительность

javascript

const obj = {};
for (let i = 0; i < 100000; i++) {
obj[`key${i}`] = i;
}

// Object.keys + forEach (быстро)
console.time("keys + forEach");
Object.keys(obj).forEach(key => {
const _ = obj[key];
});
console.timeEnd("keys + forEach");

// Object.entries + for...of (тоже быстро, но немного медленнее из-за деструктуризации)
console.time("entries + for...of");
for (const [key, value] of Object.entries(obj)) {
const _ = value;
}
console.timeEnd("entries + for...of");

// for...in + hasOwnProperty (медленнее)
console.time("for...in");
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const _ = obj[key];
}
}
console.timeEnd("for...in");

Часть 12. Визуальная шпаргалка

javascript

const obj = { a: 1, b: 2, c: 3 };

// Object.keys → ["a", "b", "c"]
// ↓ ↓ ↓
// Object.values → [1, 2, 3]
// ↓ ↓ ↓
// Object.entries → [["a",1], ["b",2], ["c",3]]

Итог: Манифест трёх методов

  1. Object.keys(obj) - массив ключей. Для перебора, проверки пустоты, получения количества свойств.
  2. Object.values(obj) - массив значений. Для суммирования, поиска максимума, фильтрации по значениям.
  3. Object.entries(obj) - массив пар. Для полного преобразования, фильтрации с доступом к ключам и значениям.
  4. Object.fromEntries(array) - обратное преобразование. Для создания объекта из массива пар.
  5. Все методы игнорируют - свойства из прототипа, неперечисляемые свойства и символы.
  6. Порядок - числовые ключи сначала (отсортированные), затем строковые (по порядку добавления).

Финальный тест (что выведет?):

javascript

const data = { b: 2, a: 1, 3: "три", 1: "один" };

console.log(Object.keys(data));
console.log(Object.values(data));
console.log(Object.entries(data));

const doubled = Object.fromEntries(
Object.entries(data).map(([k, v]) => [k, typeof v === "number" ? v * 2 : v])
);
console.log(doubled);

Ответ:

  • Object.keys: ["1", "3", "b", "a"] (числовые сначала)
  • Object.values: ["один", "три", 2, 1]
  • Object.entries: [["1", "один"], ["3", "три"], ["b", 2], ["a", 1]]
  • doubled: { "1": "один", "3": "три", b: 4, a: 2 }

Object.keys, Object.values и Object.entries - это три ключа, открывающие объекты для массиво-подобной обработки. Они превращают неповоротливые объекты в гибкие массивы, с которыми можно делать всё, что угодно: маппить, фильтровать, редуцировать, итерировать. Используйте их, и ваш код станет чище, выразительнее и современнее. А Object.fromEntries замыкает круг, позволяя превращать обработанные массивы обратно в объекты. Это идеальный пазл для работы с данными.

---------------------------------------------------------------------------------------

---------------------------------------------------------------------------------------

Распаковка реальности: Как деструктуризация изменила способ писать JavaScript

Представьте, что вы получаете коробку с подарками. Внутри - несколько предметов, каждый в своей упаковке. Вы можете распаковать коробку, достать каждый предмет по отдельности и разложить их на видное место. Или вы можете оставить всё в коробке и каждый раз, когда нужен конкретный предмет, лезть внутрь и искать его.

Деструктуризация - это искусство быстрой распаковки. Это синтаксический сахар, который позволяет извлекать данные из массивов и объектов мгновенно, в одной строке, без многословных присваиваний.

До появления деструктуризации в ES6 мы писали унылый, повторяющийся код. Теперь мы пишем код, который похож на магию. И сегодня мы разберём эту магию от А до Я.

Часть 1. Проблема: мир до деструктуризации

Представьте, что у вас есть объект с данными пользователя:

javascript

const user = {
firstName: "Анна",
lastName: "Иванова",
age: 25,
address: {
city: "Москва",
street: "Тверская"
}
};

// Как достать данные раньше?
const firstName = user.firstName;
const lastName = user.lastName;
const age = user.age;
const city = user.address.city;

// 4 строки для 4 значений! А если значений 20?

Или массив:

javascript

const colors = ["красный", "зеленый", "синий"];

// Как достать элементы раньше?
const first = colors[0];
const second = colors[1];
const third = colors[2];

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

Часть 2. Деструктуризация объектов: Распаковка по именам

2.1 Базовый синтаксис

javascript

const user = {
firstName: "Анна",
lastName: "Иванова",
age: 25
};

// Деструктуризация
const { firstName, lastName, age } = user;

console.log(firstName);
// "Анна"
console.log(lastName);
// "Иванова"
console.log(age);
// 25

Как это работает: JavaScript ищет в объекте user свойства с именами firstName, lastName, age и создаёт переменные с этими же именами.

2.2 Переименование переменных

Иногда имя свойства не подходит для переменной (например, конфликт имён или не соответствует стилю кода).

javascript

const user = { firstName: "Анна", age: 25 };

// Переименовываем firstName в name
const { firstName: name, age: userAge } = user;

console.log(name);
// "Анна"
console.log(userAge);
// 25
console.log(firstName);
// ReferenceError!

Шаблон: { исходноеИмя: новоеИмя }

2.3 Значения по умолчанию

Если свойства нет в объекте, можно задать значение по умолчанию.

javascript

const user = { firstName: "Анна" };

const { firstName, lastName = "Неизвестно", age = 0 } = user;

console.log(firstName);
// "Анна"
console.log(lastName);
// "Неизвестно"
console.log(age);
// 0

Важно: значение по умолчанию используется только если свойство отсутствует или равно undefined. Для null значение по умолчанию НЕ сработает:

javascript

const user = { firstName: null };
const { firstName = "Анна" } = user;
console.log(firstName);
// null (не "Анна"!)

2.4 Комбинация переименования и значения по умолчанию

javascript

const user = { firstName: "Анна" };

const { firstName: name = "Гость", lastName: surname = "Неизвестно" } = user;

console.log(name);
// "Анна"
console.log(surname);
// "Неизвестно"

Часть 3. Деструктуризация массивов: Распаковка по позиции

3.1 Базовый синтаксис

В отличие от объектов, где порядок не важен (мы обращаемся по имени), в массивах важен порядок - мы извлекаем по позиции.

javascript

const colors = ["красный", "зеленый", "синий"];

const [first, second, third] = colors;

console.log(first);
// "красный"
console.log(second);
// "зеленый"
console.log(third);
// "синий"

3.2 Пропуск элементов

Нужен только второй элемент? Пропустите первый!

javascript

const colors = ["красный", "зеленый", "синий"];

const [, second, third] = colors;

console.log(second);
// "зеленый"
console.log(third);
// "синий"

3.3 Значения по умолчанию

javascript

const colors = ["красный"];

const [first, second = "зеленый", third = "синий"] = colors;

console.log(first);
// "красный"
console.log(second);
// "зеленый"
console.log(third);
// "синий"

3.4 Rest-оператор (оставшиеся элементы)

javascript

const colors = ["красный", "зеленый", "синий", "желтый", "фиолетовый"];

const [first, second, ...rest] = colors;

console.log(first);
// "красный"
console.log(second);
// "зеленый"
console.log(rest);
// ["синий", "желтый", "фиолетовый"]

Rest-оператор всегда должен быть последним!

Часть 4. Вложенная деструктуризация

4.1 Вложенные объекты

javascript

const user = {
name: "Анна",
address: {
city: "Москва",
street: "Тверская",
coordinates: {
lat: 55.751244,
lng: 37.618423
}
}
};

// Деструктуризация с вложенностью
const {
name,
address: {
city,
street,
coordinates: { lat, lng }
}
} = user;

console.log(name);
// "Анна"
console.log(city);
// "Москва"
console.log(street);
// "Тверская"
console.log(lat);
// 55.751244
console.log(lng);
// 37.618423

4.2 Вложенные массивы

javascript

const matrix = [[1, 2], [3, 4], [5, 6]];

const [[a, b], [c, d], [e, f]] = matrix;

console.log(a, b, c, d, e, f);
// 1, 2, 3, 4, 5, 6

4.3 Смешанная деструктуризация (объекты в массивах, массивы в объектах)

javascript

const data = {
id: 1,
tags: ["js", "react", "node"],
author: {
name: "Анна",
posts: [10, 20, 30]
}
};

const {
id,
tags: [firstTag, secondTag],
author: {
name,
posts: [firstPost, ...restPosts]
}
} = data;

console.log(id);
// 1
console.log(firstTag);
// "js"
console.log(secondTag);
// "react"
console.log(name);
// "Анна"
console.log(firstPost);
// 10
console.log(restPosts);
// [20, 30]

Часть 5. Деструктуризация в параметрах функций

Это, пожалуй, самое полезное применение.

5.1 Объект как параметр

javascript

// Без деструктуризации
function greetUser(user) {
console.log(`Привет, ${user.name}! Тебе ${user.age} лет.`);
}

// С деструктуризацией
function greetUser({ name, age }) {
console.log(`Привет, ${name}! Тебе ${age} лет.`);
}

greetUser({ name: "Анна", age: 25 });
// "Привет, Анна! Тебе 25 лет."

5.2 Значения по умолчанию в параметрах

javascript

function createUser({ name = "Гость", age = 18, city = "Не указан" } = {}) {
return { name, age, city };
}

console.log(createUser({ name: "Анна" }));
// { name: "Анна", age: 18, city: "Не указан" }

console.log(createUser({ age: 25, city: "Москва" }));
// { name: "Гость", age: 25, city: "Москва" }

console.log(createUser());
// { name: "Гость", age: 18, city: "Не указан" }

Важно: = {} в конце гарантирует, что функция работает даже без аргументов.

5.3 Массив как параметр

javascript

function sum([a, b, c]) {
return a + b + c;
}

console.log(sum([1, 2, 3]));
// 6

5.4 Rest-параметры в функциях

javascript

function logFirstAndRest([first, ...rest]) {
console.log("Первый:", first);
console.log("Остальные:", rest);
}

logFirstAndRest([1, 2, 3, 4, 5]);
// Первый: 1
// Остальные: [2, 3, 4, 5]

Часть 6. Обмен переменных (swap)

Классический трюк: обмен значений двух переменных без временной переменной.

javascript

let a = 5;
let b = 10;

// Обмен значениями
[a, b] = [b, a];

console.log(a);
// 10
console.log(b);
// 5

Часть 7. Деструктуризация возвращаемого значения

javascript

function getCoordinates() {
return [55.751244, 37.618423];
}

function getUser() {
return {
id: 1,
name: "Анна",
email: "anna@example.com"
};
}

const [lat, lng] = getCoordinates();
const { name, email } = getUser();

console.log(lat, lng);
// 55.751244, 37.618423
console.log(name, email);
// "Анна", "anna@example.com"

Часть 8. Итерация с деструктуризацией

8.1 Object.entries + деструктуризация

javascript

const user = { name: "Анна", age: 25, city: "Москва" };

for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}
// name: Анна
// age: 25
// city: Москва

8.2 Map + деструктуризация

javascript

const map = new Map([
["name", "Анна"],
["age", 25],
["city", "Москва"]
]);

for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}

8.3 forEach с деструктуризацией

javascript

const users = [
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 },
{ name: "Вика", age: 28 }
];

users.forEach(({ name, age }) => {
console.log(`${name} (${age} лет)`);
});

Часть 9. Продвинутые техники

9.1 Деструктуризация динамических свойств

javascript

const key = "age";
const user = { name: "Анна", age: 25 };

const { [key]: userAge } = user;
console.log(userAge);
// 25

9.2 Деструктуризация примитивов

javascript

const { length, toUpperCase } = "hello";

console.log(length);
// 5
console.log(toUpperCase);
// function toUpperCase() { ... }

9.3 Деструктуризация вложенных массивов с разной длиной

javascript

const data = [1, [2, 3, [4, 5]]];

const [a, [b, c, [d, e]]] = data;

console.log(a, b, c, d, e);
// 1, 2, 3, 4, 5

9.4 Использование в return (ранний выход)

javascript

function processUser(user) {
const { name, age, isActive } = user;

if (!isActive) return { error: "Пользователь не активен" };
if (age < 18) return { error: "Несовершеннолетний" };

return { success: true, message: `Привет, ${name}` };
}

Часть 10. Реальные кейсы

10.1 Работа с API

javascript

async function fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
const { data: { user, posts, comments } } = await response.json();

return { user, posts, comments };
}

// Использование
const { user, posts } = await fetchUser(1);

10.2 Настройки компонента (React/Vue)

jsx

function Button({ type = "primary", size = "medium", onClick, children }) {
const className = `btn btn-${type} btn-${size}`;

return (
<button className={className} onClick={onClick}>
{children}
</button>
);
}

// Использование
<Button type="danger" onClick={handleClick}>
Удалить
</Button>

10.3 Импорт из модулей

javascript

// Вместо
import React from 'react';
import { useState, useEffect } from 'react';

// Можно так (но обычно пишут отдельно для читаемости)
import React, { useState, useEffect } from 'react';

10.4 Обработка форм

javascript

function handleSubmit(event) {
event.preventDefault();

// Деструктуризация FormData
const formData = new FormData(event.target);
const { username, email, password } = Object.fromEntries(formData);

console.log(username, email, password);
}

10.5 JSON.parse с деструктуризацией

javascript

const json = '{"id":1,"name":"Анна","settings":{"theme":"dark"}}';
const { id, name, settings: { theme } } = JSON.parse(json);

console.log(id, name, theme);
// 1, "Анна", "dark"

Часть 11. Подводные камни

Камень #1: Забытые скобки для объявления без переменной

javascript

// Так нельзя (SyntaxError)
{ a, b } = { a: 1, b: 2 };

// Нужно обернуть в скобки
({ a, b } = { a: 1, b: 2 });

Камень #2: Деструктуризация null или undefined

javascript

const { name } = null; // TypeError!
const [first] = undefined;
// TypeError!

// Защита
const { name } = user || {};
const [first] = arr || [];

Камень #3: Путаница между объектом и блоком кода

javascript

// Это блок кода, а не объект!
{ a, b } = { a: 1, b: 2 };
// Ошибка

// Правильно
({ a, b } = { a: 1, b: 2 });

Камень #4: Rest-оператор не работает с глубокими свойствами

javascript

const user = { name: "Анна", age: 25, city: "Москва", country: "Россия" };

// Работает
const { name, ...rest } = user;
console.log(rest);
// { age: 25, city: "Москва", country: "Россия" }

// Не работает на вложенном уровне
const { address: { city, ...restAddress } } = userWithAddress;

Часть 12. Деструктуризация vs традиционный доступ

-6

Итог: Манифест деструктуризации

  1. Деструктуризация объектов - по именам свойств. Порядок не важен.
  2. Деструктуризация массивов - по позиции. Порядок важен.
  3. Значения по умолчанию - работают при undefined, не работают при null.
  4. Переименование - { oldName: newName }.
  5. Rest-оператор (...) - собирает оставшиеся свойства/элементы.
  6. Вложенная деструктуризация - работает на любую глубину.
  7. Параметры функций - лучшее применение деструктуризации.
  8. Обмен переменных - [a, b] = [b, a].
  9. Защита от null/undefined - используйте || {} или ?? {}.

Финальный тест (проверьте себя):

javascript

const data = {
id: 1,
user: {
name: "Анна",
contacts: {
email: "anna@example.com",
phone: "+7 123 456-78-90"
}
},
tags: ["js", "react", "node"]
};

const {
id: userId,
user: {
name,
contacts: { email }
},
tags: [firstTag, ...otherTags]
} = data;

console.log(userId, name, email, firstTag, otherTags);

Ответ: 1, "Анна", "anna@example.com", "js", ["react", "node"]

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

Используйте деструктуризацию каждый день. Она сделает ваш код лучше. А когда вы привыкнете, код без неё покажется вам ужасно многословным. И это правильно.

--------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------

Повелители времени: Полное руководство по работе с датами в JavaScript

Время - самая сложная субстанция в программировании. Часовые пояса, переходы на летнее время, високосные годы, разные форматы... Ошибка в работе с датами может привести к тому, что билет на самолёт окажется на день позже, а уведомление придёт в 3 часа ночи.

JavaScript имеет встроенный объект Date, который предоставляет базовые возможности для работы с датами и временем. Но он полон сюрпризов, подводных камней и исторических странностей. Месяцы считаются с 0, годы - с 1900, а getDay() возвращает день недели, а не дату.

Сегодня мы разберём все тонкости работы с датами: от создания до форматирования, от вычислений до работы с часовыми поясами. И, конечно, поговорим о современной библиотеке Temporal, которая должна заменить старый добрый Date.

Часть 1. Объект Date: Краткая история

Объект Date появился ещё в самом первом JavaScript в 1995 году. Он был скопирован из Java (отсюда и название, и странная индексация месяцев). С тех пор он почти не менялся, а его недостатки пытаются исправить добавлением новых методов.

javascript

const now = new Date();
console.log(now);
// "Sun Apr 13 2026 15:30:00 GMT+0700 (Красноярск)"

Что важно знать о Date:

  • Хранит количество миллисекунд с 1 января 1970 года UTC (Unix Epoch)
  • Поддерживает часовые пояса (но методы в основном возвращают локальное время)
  • Месяцы считаются с 0 (январь = 0, декабрь = 11)
  • Дни недели: воскресенье = 0, суббота = 6

Часть 2. Создание дат: 5 способов

2.1 Текущая дата и время

javascript

const now = new Date();
console.log(now);

2.2 Из строки (не рекомендуется - зависит от реализации)

javascript

const date1 = new Date("2024-01-15"); // 15 января 2024, UTC
const date2 = new Date("January 15, 2024");
// локальная дата
const date3 = new Date("2024-01-15T12:00:00");
// ISO строка

// ⚠️ Осторожно! Разные браузеры могут по-разному парсить строки

2.3 Из компонентов (год, месяц, день, часы...)

javascript

// Полная форма (год, месяц, день, час, минута, секунда, миллисекунда)
const date = new Date(2024, 0, 15, 14, 30, 0, 0);
console.log(date);
// 15 января 2024, 14:30:00 (локальное время)

// Месяцы с 0! (0 = январь, 11 = декабрь)
// Минимально: год, месяц
const date2 = new Date(2024, 0);
// 1 января 2024

// Если параметров больше, чем нужно - OK
// Если меньше - недостающие заполняются минимальными значениями

2.4 Из таймстампа (миллисекунд с 1970 года)

javascript

const timestamp = Date.now(); // текущий таймстамп
const date = new Date(timestamp);

2.5 Пустой конструктор (текущая дата)

javascript

const now = new Date();

Часть 3. Получение компонентов даты

3.1 Основные геттеры (локальное время)

javascript

const date = new Date(2024, 0, 15, 14, 30, 45, 123);

console.log(date.getFullYear());
// 2024 (год)
console.log(date.getMonth());
// 0 (январь! 0-11)
console.log(date.getDate());
// 15 (день месяца, 1-31)
console.log(date.getDay());
// 1 (день недели: 0 = вс, 1 = пн, ..., 6 = сб)
console.log(date.getHours());
// 14 (часы, 0-23)
console.log(date.getMinutes());
// 30 (минуты, 0-59)
console.log(date.getSeconds());
// 45 (секунды, 0-59)
console.log(date.getMilliseconds());
// 123 (миллисекунды, 0-999)

3.2 UTC-версии (мировое время)

javascript

console.log(date.getUTCFullYear()); // год в UTC
console.log(date.getUTCMonth());
// месяц в UTC (0-11)
console.log(date.getUTCDate());
// день месяца в UTC
console.log(date.getUTCHours());
// часы в UTC
// и так далее...

3.3 Таймстамп

javascript

console.log(date.getTime()); // миллисекунды с 1970
console.log(Date.now());
// текущий таймстамп (без создания объекта)

Часть 4. Установка компонентов даты

Все геттеры имеют парные сеттеры:

javascript

const date = new Date(2024, 0, 15);

date.setFullYear(2025);
date.setMonth(5);
// июнь (5 = июнь, так как январь = 0)
date.setDate(20);
date.setHours(10);
date.setMinutes(30);
date.setSeconds(0);

console.log(date);
// 20 июня 2025, 10:30:00

// Сеттеры возвращают таймстамп
const timestamp = date.setFullYear(2026);
console.log(timestamp);
// число миллисекунд

Часть 5. Форматирование дат

5.1 Базовые методы

javascript

const date = new Date(2024, 0, 15, 14, 30, 0);

console.log(date.toString());
// "Mon Jan 15 2024 14:30:00 GMT+0700 (Красноярск)"
console.log(date.toDateString());
// "Mon Jan 15 2024"
console.log(date.toTimeString());
// "14:30:00 GMT+0700 (Красноярск)"
console.log(date.toISOString());
// "2024-01-15T07:30:00.000Z" (UTC)
console.log(date.toUTCString());
// "Mon, 15 Jan 2024 07:30:00 GMT"
console.log(date.toLocaleString());
// "15.01.2024, 14:30:00" (зависит от локали)
console.log(date.toLocaleDateString());
// "15.01.2024"
console.log(date.toLocaleTimeString());
// "14:30:00"

5.2 toLocaleString с параметрами

javascript

const date = new Date(2024, 0, 15, 14, 30, 0);

// Русская локаль
console.log(date.toLocaleString('ru-RU'));
// "15.01.2024, 14:30:00"

// Американская локаль
console.log(date.toLocaleString('en-US'));
// "1/15/2024, 2:30:00 PM"

// Японская локаль
console.log(date.toLocaleString('ja-JP'));
// "2024/1/15 14:30:00"

// С опциями форматирования
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit'
};
console.log(date.toLocaleString('ru-RU', options));
// "понедельник, 15 января 2024 г., 14:30"

5.3 Intl.DateTimeFormat - профессиональное форматирование

javascript

const date = new Date(2024, 0, 15, 14, 30, 0);

const formatter = new Intl.DateTimeFormat('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});

console.log(formatter.format(date));
// "15 января 2024 г., 14:30"

// Массовое форматирование
const dates = [date, new Date(2024, 1, 20)];
console.log(formatter.formatRange(dates[0], dates[1]));
// "15 января 2024 г., 14:30 – 20 февраля 2024 г., 00:00"

Часть 6. Вычисления с датами

6.1 Разница между датами

javascript

const start = new Date(2024, 0, 1); // 1 января 2024
const end = new Date(2024, 0, 15);
// 15 января 2024

const diffMs = end - start;
// разница в миллисекундах
console.log(diffMs);
// 1209600000 (14 дней * 24 * 60 * 60 * 1000)

const diffDays = diffMs / (1000 * 60 * 60 * 24);
console.log(diffDays);
// 14

6.2 Добавление времени

javascript

const date = new Date(2024, 0, 15);

// Добавляем 7 дней
date.setDate(date.getDate() + 7);
console.log(date);
// 22 января 2024

// Добавляем 3 месяца
date.setMonth(date.getMonth() + 3);
console.log(date);
// 22 апреля 2024

// Добавляем 1 год
date.setFullYear(date.getFullYear() + 1);
console.log(date);
// 22 апреля 2025

Важно: JavaScript сам обрабатывает переполнение:

javascript

const date = new Date(2024, 0, 31); // 31 января 2024

date.setMonth(date.getMonth() + 1);
// +1 месяц
console.log(date);
// 2 марта 2024 (не 31 февраля, потому что февраля не было!)

6.3 Сравнение дат

javascript

const date1 = new Date(2024, 0, 15);
const date2 = new Date(2024, 0, 20);
const date3 = new Date(2024, 0, 15);

console.log(date1 < date2);
// true
console.log(date1 > date2);
// false
console.log(date1 === date3);
// false! (разные объекты)
console.log(date1.getTime() === date3.getTime());
// true (сравниваем таймстампы)

Часть 7. Подводные камни объекта Date

Камень #1: Месяцы с 0

javascript

const date = new Date(2024, 0, 15);
console.log(date.getMonth());
// 0 (январь)

// Ошибка новичка
const birthday = new Date(2024, 12, 31);
// 12 = январь 2025!

Камень #2: getDay() возвращает день недели, а не дату

javascript

const date = new Date(2024, 0, 15);
console.log(date.getDay());
// 1 (понедельник)
console.log(date.getDate());
// 15 (дата) - вот что нужно!

Камень #3: Парсинг строк зависит от браузера

javascript

// Может работать, может нет
const date = new Date("2024-01-15");
// ISO - безопасно
const bad = new Date("15/01/2024");
// в США это 15 июня, в Европе - 15 января

Правило: Используйте ISO формат YYYY-MM-DD или парсите вручную.

Камень #4: Таймстамп в секундах, а не миллисекундах

javascript

// Многие API возвращают время в СЕКУНДАХ
const apiTimestamp = 1705315200;
// 15 января 2024 в секундах

// Неправильно (будет в 1970 году)
const wrong = new Date(apiTimestamp);

// Правильно (умножаем на 1000)
const correct = new Date(apiTimestamp * 1000);

Камень #5: Часовые пояса

javascript

// ISO строка интерпретируется как UTC
const iso = new Date("2024-01-15");
console.log(iso.getHours());
// 0 (UTC) → может отличаться от ожидаемого

// Для локальной даты используйте компоненты
const local = new Date(2024, 0, 15);
console.log(local.getHours());
// 0 (локальное)

Часть 8. Полезные утилиты

8.1 Получение начала/конца дня

javascript

function startOfDay(date) {
const result = new Date(date);
result.setHours(0, 0, 0, 0);
return result;
}

function endOfDay(date) {
const result = new Date(date);
result.setHours(23, 59, 59, 999);
return result;
}

const date = new Date(2024, 0, 15, 14, 30);
console.log(startOfDay(date));
// 15 января 2024, 00:00:00
console.log(endOfDay(date));
// 15 января 2024, 23:59:59.999

8.2 Проверка на валидность даты

javascript

function isValidDate(date) {
return date instanceof Date && !isNaN(date);
}

console.log(isValidDate(new Date(2024, 0, 15)));
// true
console.log(isValidDate(new Date("invalid")));
// false
console.log(isValidDate(new Date(2024, 13, 15)));
// true (съехал на следующий год)

8.3 Форматирование в YYYY-MM-DD

javascript

function formatYMD(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}

console.log(formatYMD(new Date(2024, 0, 15)));
// "2024-01-15"

8.4 Количество дней в месяце

javascript

function daysInMonth(year, month) {
// month: 0-11
return new Date(year, month + 1, 0).getDate();
}

console.log(daysInMonth(2024, 1));
// 29 (февраль високосного)
console.log(daysInMonth(2023, 1));
// 28

Часть 9. Библиотеки для работы с датами

Объект Date имеет много недостатков. Поэтому в реальных проектах часто используют библиотеки.

9.1 date-fns (модульная, современная)

javascript

import { format, differenceInDays, addDays, isWeekend } from 'date-fns';

const date = new Date(2024, 0, 15);

console.log(format(date, 'dd.MM.yyyy'));
// "15.01.2024"
console.log(addDays(date, 7));
// 22 января 2024
console.log(isWeekend(date));
// false (понедельник)

9.2 Day.js (лёгкая альтернатива Moment)

javascript

import dayjs from 'dayjs';

const date = dayjs('2024-01-15');
console.log(date.format('DD/MM/YYYY'));
// "15/01/2024"
console.log(date.add(7, 'day'));
// +7 дней
console.log(date.diff('2024-01-01', 'day'));
// 14

9.3 Moment.js (старая, но всё ещё популярная)

javascript

// ⚠️ Устаревшая, не рекомендуется для новых проектов
const date = moment('2024-01-15');
console.log(date.format('DD.MM.YYYY'));

Часть 10. Temporal - будущее работы с датами

Temporal - это новый API для работы с датами и временем, который должен заменить устаревший Date. Он находится на стадии предложения (stage 3) и скоро появится в JavaScript.

javascript

// Псевдокод (когда Temporal будет готов)
// Не работает в текущих браузерах!

// PlainDate - дата без времени и часового пояса
const date = new Temporal.PlainDate(2024, 1, 15);
// 15 января 2024
console.log(date.toString());
// "2024-01-15"

// PlainTime - время без даты
const time = new Temporal.PlainTime(14, 30, 0);

// PlainDateTime - дата и время
const datetime = new Temporal.PlainDateTime(2024, 1, 15, 14, 30);

// ZonedDateTime - с часовым поясом
const zoned = new Temporal.ZonedDateTime(
1705315200000000000n,
'Europe/Moscow'
);

// Удобные операции
const tomorrow = date.add({ days: 1 });
const diff = date.until(tomorrow, { largestUnit: 'day' });
console.log(diff.days);
// 1

Часть 11. Реальные кейсы

11.1 Счётчик дней до события

javascript

function daysUntil(targetDate) {
const today = new Date();
today.setHours(0, 0, 0, 0);

const target = new Date(targetDate);
target.setHours(0, 0, 0, 0);

const diff = target - today;
return Math.ceil(diff / (1000 * 60 * 60 * 24));
}

console.log(daysUntil('2024-12-31'));
// сколько дней до Нового года

11.2 Форматирование относительного времени

javascript

function timeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);

const intervals = [
{ label: 'год', seconds: 31536000, divisor: 31536000 },
{ label: 'месяц', seconds: 2592000, divisor: 2592000 },
{ label: 'день', seconds: 86400, divisor: 86400 },
{ label: 'час', seconds: 3600, divisor: 3600 },
{ label: 'минуту', seconds: 60, divisor: 60 }
];

for (const interval of intervals) {
const count = Math.floor(seconds / interval.seconds);
if (count >= 1) {
const plural = count > 1 ? `${interval.label}ов` : interval.label;
return `${count} ${plural} назад`;
}
}

return 'только что';
}

console.log(timeAgo(new Date(Date.now() - 1000 * 60 * 5)));
// "5 минут назад"

11.3 Генерация календаря

javascript

function getMonthDays(year, month) {
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);

const startDayOfWeek = firstDay.getDay();
// 0 = вс, 6 = сб
const daysInMonth = lastDay.getDate();

const calendar = [];

// Пустые дни в начале
for (let i = 0; i < startDayOfWeek; i++) {
calendar.push(null);
}

// Дни месяца
for (let i = 1; i <= daysInMonth; i++) {
calendar.push(new Date(year, month, i));
}

return calendar;
}

const january2024 = getMonthDays(2024, 0);
console.log(january2024);
// Массив с null для пустых дней и дат для дней месяца

Итог: Манифест дат

  1. Месяцы с 0 - январь = 0, декабрь = 11.
  2. getDay() - день недели (0 = вс), getDate() — день месяца.
  3. ISO формат (YYYY-MM-DD) - самый безопасный для парсинга.
  4. Таймстамп - миллисекунды с 1970 года (не забывайте про *1000).
  5. Сравнение - используйте getTime(), не сравнивайте объекты напрямую.
  6. setDate / setMonth - корректно обрабатывают переполнение.
  7. Библиотеки - для сложных проектов используйте date-fns или dayjs.
  8. Temporal - будущее (ждём с нетерпением).

Финальный тест (проверьте себя):

javascript

const date1 = new Date(2024, 0, 15);
const date2 = new Date("2024-01-15");

console.log(date1.getMonth());
console.log(date1.getDate());
console.log(date2.getHours());
console.log(date1 === date2);
console.log(date1.getTime() === date2.getTime());

const date3 = new Date(2024, 0, 31);
date3.setMonth(1);
console.log(date3.getDate());

Ответы: 0, 15, 0 (ISO в UTC), false (разные объекты), false (разные часовые пояса), 2 (март, потому что 31 февраля не существует).

Работа с датами и временем - это искусство. Оно требует внимания к деталям, понимания часовых поясов и знания подводных камней. Но когда вы освоите Date и его методы, вы сможете решать любые задачи, связанные со временем. А когда Temporal станет стандартом, работа с датами станет ещё проще и приятнее. А пока - помните про месяцы с нуля и не забывайте проверять часовые пояса.

-------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------

Текстовый портал: Как JSON стал универсальным языком обмена данными

Представьте, что вам нужно передать сложный объект из одного языка программирования в другой. Или сохранить состояние приложения на диск. Или отправить данные на сервер. Как это сделать? Никто не гарантирует, что Python поймёт структуру памяти JavaScript, а Java - внутреннее представление PHP.

Нужен универсальный язык. Текстовый. Простой. Понятный и людям, и компьютерам.

Встречайте JSON (JavaScript Object Notation) - формат, который начал жизнь как способ записи объектов JavaScript, а стал стандартом де-факто для обмена данными во всём вебе.

Сегодня мы разберём, как работать с JSON в JavaScript: превращать объекты в строки, парсить строки обратно, настраивать процесс сериализации и использовать мощный метод toJSON.

Часть 1. Что такое JSON?

JSON - это текстовый формат обмена данными, основанный на синтаксисе JavaScript-объектов. Он лёгкий, читаемый человеком и поддерживается практически всеми языками программирования.

json

{
"name": "Анна",
"age": 25,
"isStudent": false,
"address": {
"city": "Москва",
"street": "Тверская"
},
"hobbies": ["чтение", "музыка", "спорт"]
}

Важно: JSON - это текст, а не объект. Он может быть сохранён в файле, отправлен по сети, записан в базу данных.

Часть 2. JSON vs JavaScript объекты: В чём разница?

На первый взгляд JSON очень похож на JavaScript-объекты. Но есть важные отличия:

-7

javascript

// JavaScript объект (валидный)
const obj = {
name: "Анна",
// ключ без кавычек
'last-name': "Иванова",
// одинарные кавычки
age: 25,
greet() { console.log("Hi"); },
// метод
data: undefined
// undefined
};

// JSON (строгий формат)
const json = `{
"name": "Анна",
"last-name": "Иванова",
"age": 25
}`;

Часть 3. Главные методы: JSON.stringify и JSON.parse

3.1 JSON.stringify - объект → строка

Превращает JavaScript-значение в JSON-строку.

javascript

const user = {
name: "Анна",
age: 25,
isStudent: false,
address: {
city: "Москва",
street: "Тверская"
},
hobbies: ["чтение", "музыка"]
};

const jsonString = JSON.stringify(user);
console.log(jsonString);
// {"name":"Анна","age":25,"isStudent":false,"address":{"city":"Москва","street":"Тверская"},"hobbies":["чтение","музыка"]}

3.2 JSON.parse - строка → объект

Превращает JSON-строку обратно в JavaScript-объект.

javascript

const jsonString = '{"name":"Анна","age":25,"city":"Москва"}';
const user = JSON.parse(jsonString);
console.log(user.name);
// "Анна"
console.log(user.age);
// 25

Часть 4. Тонкая настройка JSON.stringify

4.1 Параметр replacer (фильтр/преобразователь)

Может быть массивом ключей или функцией.

Как массив (выбираем только нужные поля):

javascript

const user = {
id: 1,
name: "Анна",
password: "secret123",
email: "anna@example.com",
age: 25
};

// Сериализуем только id, name и email (без пароля!)
const safe = JSON.stringify(user, ["id", "name", "email"]);
console.log(safe);
// {"id":1,"name":"Анна","email":"anna@example.com"}

Как функция (преобразуем значения):

javascript

const user = {
name: "Анна",
age: 25,
password: "secret123"
};

const json = JSON.stringify(user, (key, value) => {
// Скрываем пароль
if (key === "password") return undefined;
// Преобразуем строки в верхний регистр
if (typeof value === "string") return value.toUpperCase();
return value;
});

console.log(json);
// {"name":"АННА","age":25}

4.2 Параметр space (форматирование)

Добавляет отступы для читаемости (pretty print).

javascript

const user = { name: "Анна", age: 25, city: "Москва" };

// Без форматирования (всё в одну строку)
console.log(JSON.stringify(user));
// {"name":"Анна","age":25,"city":"Москва"}

// С отступами в 2 пробела
console.log(JSON.stringify(user, null, 2));
// {
// "name": "Анна",
// "age": 25,
// "city": "Москва"
// }

// С отступом в виде табуляции
console.log(JSON.stringify(user, null, "\t"));
// {
// "name": "Анна",
// "age": 25,
// "city": "Москва"
// }

4.3 Форматирование с replacer и space

javascript

const user = {
id: 1,
name: "Анна",
password: "secret",
createdAt: new Date(2024, 0, 15)
};

const json = JSON.stringify(
user,
(key, value) => {
if (key === "password") return undefined;
if (value instanceof Date) return value.toISOString();
return value;
},
2
);

console.log(json);
// {
// "id": 1,
// "name": "Анна",
// "createdAt": "2024-01-14T17:00:00.000Z"
// }

Часть 5. Метод toJSON - кастомная сериализация

Если объект имеет метод toJSON, JSON.stringify вызывает его вместо стандартной сериализации.

javascript

const user = {
firstName: "Анна",
lastName: "Иванова",
age: 25,
password: "secret",

// Кастомная сериализация
toJSON() {
return {
fullName: `${this.firstName} ${this.lastName}`,
age: this.age,
// Пароль не включаем!
};
}
};

console.log(JSON.stringify(user));
// {"fullName":"Анна Иванова","age":25}

5.1 toJSON для дат

Встроенные объекты уже имеют toJSON:

javascript

const date = new Date(2024, 0, 15, 12, 0, 0);
console.log(date.toJSON());
// "2024-01-15T05:00:00.000Z"
console.log(JSON.stringify(date));
// "2024-01-15T05:00:00.000Z"

5.2 toJSON для сложных объектов

javascript

class Product {
constructor(name, price, discount) {
this.name = name;
this.price = price;
this.discount = discount;
}

getFinalPrice() {
return this.price * (1 - this.discount / 100);
}

toJSON() {
return {
name: this.name,
price: this.price,
finalPrice: this.getFinalPrice(),
discount: this.discount
};
}
}

const product = new Product("Ноутбук", 50000, 10);
console.log(JSON.stringify(product));
// {"name":"Ноутбук","price":50000,"finalPrice":45000,"discount":10}

5.3 toJSON для циклических структур

javascript

const node = {
name: "root",
children: []
};

const child = { name: "child", parent: node };
node.children.push(child);

// Обычная сериализация вызовет ошибку (циклическая ссылка)
// JSON.stringify(node); // TypeError: Converting circular structure to JSON

// Спасаем через toJSON
node.toJSON = function() {
return {
name: this.name,
children: this.children.map(child => ({
name: child.name
// parent не включаем, чтобы разорвать цикл
}))
};
};

console.log(JSON.stringify(node));
// {"name":"root","children":[{"name":"child"}]}

Часть 6. Что не сериализуется в JSON?

6.1 undefined, функции, символы

javascript

const obj = {
a: undefined,
// пропускается
b: function() {},
// пропускается
c: Symbol("id"),
// пропускается
d: null,
// сохраняется как null
e: 42
// сохраняется
};

console.log(JSON.stringify(obj));
// {"d":null,"e":42}

6.2 NaN, Infinity

javascript

const obj = {
a: NaN,
// становится null
b: Infinity,
// становится null
c: -Infinity
// становится null
};

console.log(JSON.stringify(obj));
// {"a":null,"b":null,"c":null}

6.3 Date (становится строкой)

javascript

const obj = {
date: new Date(2024, 0, 15)
};

console.log(JSON.stringify(obj));
// {"date":"2024-01-14T17:00:00.000Z"}

6.4 RegExp, Error, Map, Set

javascript

const obj = {
regex: /hello/g,
// становится {}
error: new Error("Oops"),
// становится {}
map: new Map([["a", 1]]),
// становится {}
set: new Set([1, 2, 3])
// становится {}
};

console.log(JSON.stringify(obj));
// {"regex":{},"error":{},"map":{},"set":{}}

Часть 7. Обратное преобразование: JSON.parse с reviver

JSON.parse может принимать функцию reviver, которая преобразует значения во время парсинга.

javascript

const json = '{"name":"Анна","birthday":"2024-01-15T00:00:00.000Z","price":"100"}';

const data = JSON.parse(json, (key, value) => {
// Преобразуем ISO-строку в Date
if (key === "birthday" && typeof value === "string") {
return new Date(value);
}
// Преобразуем строку с ценой в число
if (key === "price") {
return Number(value);
}
return value;
});

console.log(data.birthday instanceof Date);
// true
console.log(data.price);
// 100 (число, не строка)

Часть 8. Обработка ошибок

JSON.parse выбрасывает исключение при некорректном JSON. Всегда оборачивайте в try...catch.

javascript

function safeParse(jsonString, defaultValue = null) {
try {
return JSON.parse(jsonString);
} catch (error) {
console.error("Ошибка парсинга JSON:", error.message);
return defaultValue;
}
}

const badJson = '{name: "Анна"}';
// Нет кавычек у ключа!
const result = safeParse(badJson, {});
console.log(result);
// {}

const goodJson = '{"name":"Анна"}';
const user = safeParse(goodJson);
console.log(user);
// { name: "Анна" }

Часть 9. Реальные кейсы

9.1 Сохранение состояния в localStorage

javascript

const appState = {
user: { name: "Анна", isLoggedIn: true },
theme: "dark",
lastVisit: new Date()
};

// Сохраняем
localStorage.setItem("appState", JSON.stringify(appState));

// Загружаем
const saved = localStorage.getItem("appState");
if (saved) {
const state = JSON.parse(saved);
// Преобразуем строку даты обратно в Date
state.lastVisit = new Date(state.lastVisit);
console.log(state);
}

9.2 Отправка данных на сервер

javascript

async function saveUser(user) {
const response = await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
});

return response.json();
}

const user = { name: "Анна", age: 25 };
saveUser(user);

9.3 Глубокое копирование объектов (с ограничениями)

javascript

const original = {
name: "Анна",
address: { city: "Москва", street: "Тверская" },
hobbies: ["чтение", "музыка"]
};

// Глубокая копия через JSON
const copy = JSON.parse(JSON.stringify(original));

copy.address.city = "СПб";
console.log(original.address.city);
// "Москва" (не изменилось!)

// Но помните о ограничениях: функции, undefined, Date теряются

9.4 Конфигурация приложения

javascript

// config.json (файл)
{
"apiUrl": "https://api.example.com",
"timeout": 5000,
"features": {
"darkMode": true,
"notifications": false
}
}

// В коде
import config from './config.json' assert { type: "json" };
// или через fetch
const response = await fetch('/config.json');
const config = await response.json();

9.5 Сериализация ошибок для логирования

javascript

function serializeError(error) {
return JSON.stringify(error, Object.getOwnPropertyNames(error));
}

try {
throw new Error("Что-то пошло не так");
} catch (error) {
console.log(serializeError(error));
// {"stack":"Error: Что-то пошло не так\n at ...","message":"Что-то пошло не так"}
}

Часть 10. JSON и глубокое копирование: ограничения

JSON-копирование - быстрый способ глубокого копирования, но с важными ограничениями:

javascript

const original = {
date: new Date(),
func: () => console.log("hi"),
undef: undefined,
inf: Infinity,
regex: /hello/g,
nested: { a: 1 }
};

const copy = JSON.parse(JSON.stringify(original));

console.log(copy.date instanceof Date);
// false (строка)
console.log(copy.func);
// undefined
console.log(copy.undef);
// undefined (свойство пропало)
console.log(copy.inf);
// null
console.log(copy.regex);
// {}
console.log(copy.nested);
// { a: 1 } (работает)

Когда использовать JSON-копирование:

  • Данные без функций, Date, undefined, Infinity, RegExp
  • Нет циклических ссылок
  • Нужна быстрая и простая глубокая копия

Часть 11. JSON vs другие форматы

-8

Часть 12. Подводные камни

Камень #1: Трейлинг-запятые запрещены

javascript

// ❌ Ошибка!
const json = '{"a": 1, "b": 2,}';
JSON.parse(json);
// SyntaxError

// ✅ Правильно
const json = '{"a": 1, "b": 2}';

Камень #2: Нет комментариев

javascript

// ❌ Ошибка!
const json = `{
"name": "Анна", // это комментарий
"age": 25
}`;

Камень #3: Ключи только в двойных кавычках

javascript

// ❌ Ошибка!
const json = "{name: 'Анна'}";

// ❌ Ошибка!
const json = "{'name': 'Анна'}";

// ✅ Правильно
const json = '{"name": "Анна"}';

Камень #4: Потеря точности больших чисел

javascript

const obj = { big: 9007199254740992 };
const json = JSON.stringify(obj);
const parsed = JSON.parse(json);
console.log(parsed.big);
// 9007199254740992 (ещё точно)

const obj2 = { big: 9007199254740993 };
const json2 = JSON.stringify(obj2);
const parsed2 = JSON.parse(json2);
console.log(parsed2.big);
// 9007199254740992 (потеря точности!)

Часть 13. Полезные трюки

13.1 Удаление falsy значений при сериализации

javascript

const data = {
name: "Анна",
age: 0,
// хотим сохранить
city: "",
// хотим сохранить
temp: null,
// хотим удалить
undef: undefined,
flag: false
// хотим сохранить
};

const clean = JSON.stringify(data, (key, value) => {
if (value === null || value === undefined) return undefined;
return value;
});

console.log(clean);
// {"name":"Анна","age":0,"city":"","flag":false}

13.2 Форматирование для логов

javascript

function prettyLog(obj) {
console.log(JSON.stringify(obj, null, 2));
}

prettyLog({ name: "Анна", age: 25, address: { city: "Москва" } });
// {
// "name": "Анна",
// "age": 25,
// "address": {
// "city": "Москва"
// }
// }

13.3 Восстановление дат при парсинге

javascript

function parseWithDates(jsonString) {
return JSON.parse(jsonString, (key, value) => {
// Проверяем, похоже ли значение на ISO-дату
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
const date = new Date(value);
if (!isNaN(date)) return date;
}
return value;
});
}

const json = '{"name":"Анна","created":"2024-01-15T12:00:00.000Z"}';
const data = parseWithDates(json);
console.log(data.created instanceof Date);
// true

Итог: Манифест JSON

  1. JSON - текст, не объект. Ключи в двойных кавычках, строки в двойных кавычках.
  2. JSON.stringify() - объект → строка. Принимает replacer и space.
  3. JSON.parse() - строка → объект. Принимает reviver.
  4. toJSON() - кастомная сериализация для объектов.
  5. Что не сериализуется: undefined, функции, символы, NaN, Infinity (становятся null или пропадают).
  6. Даты становятся строками (ISO формат).
  7. Всегда используйте try...catch при парсинге JSON.
  8. JSON не для всего - не подходит для бинарных данных, циклических ссылок, больших чисел.

Финальный тест (что выведет?):

javascript

const obj = {
a: undefined,
b: null,
c: 42,
d: NaN,
e: new Date(2024, 0, 15),
f: () => {},
g: [1, 2, undefined, 3],
toJSON() {
return { custom: true };
}
};

console.log(JSON.stringify(obj));

const json = '{"date":"2024-01-15T12:00:00.000Z","price":"100"}';
const parsed = JSON.parse(json, (key, value) => {
if (key === "date") return new Date(value);
if (key === "price") return Number(value);
return value;
});
console.log(parsed.price + 50);

Ответы:

  • '{"b":null,"c":42,"d":null,"e":"2024-01-14T17:00:00.000Z","g":[1,2,null,3],"custom":true}'
    (a и f пропали, NaN → null, toJSON сработал)
  • 150 (price из строки "100" стал числом 100)

JSON - это не просто формат. Это мост между системами, языками и платформами. Понимание того, как работают JSON.stringify и JSON.parse, а также умение настраивать сериализацию через toJSON и replacer/reviver, делает вас профессионалом, способным эффективно управлять данными в любом приложении. Используйте JSON правильно, и ваши данные всегда будут в безопасности.