Два короля коллекций: Полное руководство по 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 против обычного объекта: битва титанов
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
- Map - коллекция ключ-значение с ключами любого типа, сохраняет порядок.
- Set - коллекция уникальных значений любого типа.
- Ключи в Map - не приводятся к строкам (в отличие от объектов).
- Размер - .size, не .length.
- Итерация - for...of, keys(), values(), entries(), forEach.
- Удаление дубликатов из массива - [...new Set(array)].
- WeakMap / WeakSet - для данных, которые должны умереть вместе с объектом.
- Производительность - 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
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. Зачем нужны 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-коллекций
- WeakMap и WeakSet хранят слабые ссылки - не мешают сборщику мусора.
- Ключи - только объекты - никаких примитивов.
- Не итерируются - нет keys(), values(), entries(), forEach, for...of.
- Нет .size - размер неизвестен и ненадёжен.
- Нет .clear() - очистка происходит автоматически.
- Идеальны для приватных данных - данные живут, пока жив объект.
- Идеальны для кэширования - автоматическая очистка без утечек.
- Идеальны для метаданных - не загрязняют объекты.
Финальный тест (что выведет?):
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]]
Часть 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:
- Сначала числовые ключи в порядке возрастания
- Затем строковые ключи в порядке добавления
- Затем символы (не включаются в эти методы)
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]]
Итог: Манифест трёх методов
- Object.keys(obj) - массив ключей. Для перебора, проверки пустоты, получения количества свойств.
- Object.values(obj) - массив значений. Для суммирования, поиска максимума, фильтрации по значениям.
- Object.entries(obj) - массив пар. Для полного преобразования, фильтрации с доступом к ключам и значениям.
- Object.fromEntries(array) - обратное преобразование. Для создания объекта из массива пар.
- Все методы игнорируют - свойства из прототипа, неперечисляемые свойства и символы.
- Порядок - числовые ключи сначала (отсортированные), затем строковые (по порядку добавления).
Финальный тест (что выведет?):
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 традиционный доступ
Итог: Манифест деструктуризации
- Деструктуризация объектов - по именам свойств. Порядок не важен.
- Деструктуризация массивов - по позиции. Порядок важен.
- Значения по умолчанию - работают при undefined, не работают при null.
- Переименование - { oldName: newName }.
- Rest-оператор (...) - собирает оставшиеся свойства/элементы.
- Вложенная деструктуризация - работает на любую глубину.
- Параметры функций - лучшее применение деструктуризации.
- Обмен переменных - [a, b] = [b, a].
- Защита от 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 для пустых дней и дат для дней месяца
Итог: Манифест дат
- Месяцы с 0 - январь = 0, декабрь = 11.
- getDay() - день недели (0 = вс), getDate() — день месяца.
- ISO формат (YYYY-MM-DD) - самый безопасный для парсинга.
- Таймстамп - миллисекунды с 1970 года (не забывайте про *1000).
- Сравнение - используйте getTime(), не сравнивайте объекты напрямую.
- setDate / setMonth - корректно обрабатывают переполнение.
- Библиотеки - для сложных проектов используйте date-fns или dayjs.
- 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-объекты. Но есть важные отличия:
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 другие форматы
Часть 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
- JSON - текст, не объект. Ключи в двойных кавычках, строки в двойных кавычках.
- JSON.stringify() - объект → строка. Принимает replacer и space.
- JSON.parse() - строка → объект. Принимает reviver.
- toJSON() - кастомная сериализация для объектов.
- Что не сериализуется: undefined, функции, символы, NaN, Infinity (становятся null или пропадают).
- Даты становятся строками (ISO формат).
- Всегда используйте try...catch при парсинге JSON.
- 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 правильно, и ваши данные всегда будут в безопасности.