Всё есть объект (почти): Путеводитель по вселенной JavaScript-объектов
Вы думаете, что знаете объекты? { key: value } - что тут сложного? Но JavaScript-объекты скрывают тайны, о которых вы не догадывались. Они могут создаваться без прототипа, иметь вычисляемые ключи, скрытые свойства и даже перехватывать любое обращение к себе.
Объекты в JavaScript - это не просто "словари" из других языков. Это динамические, гибкие, живые структуры, которые могут меняться прямо во время выполнения. И если массивы - это просто разновидность объектов, а функции - это объекты с возможностью вызова, то понимание объектов открывает дверь к пониманию всего языка.
Часть 1. Что такое объект? Переосмысление
В JavaScript объект - это коллекция свойств. Свойство - это связь между ключом (строкой или символом) и значением (любым типом).
javascript
const user = {
name: "Анна", // свойство с ключом "name"
age: 25, // свойство с ключом "age"
"has cat": true // ключ с пробелом (требует кавычек)
};
// Доступ через точку
console.log(user.name); // "Анна"
// Доступ через квадратные скобки (для любых ключей)
console.log(user["has cat"]); // true
console.log(user["age"]); // 25
Важно: Ключи объектов - всегда строки (или символы). Даже если вы используете число, оно превратится в строку.
javascript
const obj = {};
obj[1] = "один";
obj["1"] = "два"; // перезапишет предыдущее!
console.log(obj); // { "1": "два" }
Часть 2. Создание объектов: 5 способов (и когда какой использовать)
2.1 Литерал объекта {} - самый простой и быстрый
javascript
const obj = {
key: "value",
method() { // сокращённая запись метода
return this.key;
}
};
2.2 Конструктор new Object() - почти не используется
javascript
const obj = new Object();
obj.name = "Анна";
2.3 Object.create() - для тонкого управления прототипом
javascript
const proto = { greet() { return "Привет"; } };
const obj = Object.create(proto);
obj.name = "Анна";
console.log(obj.greet()); // "Привет" (метод из прототипа)
2.4 Классы (синтаксический сахар над прототипами)
javascript
class User {
constructor(name) {
this.name = name;
}
greet() {
return `Привет, ${this.name}`;
}
}
const obj = new User("Анна");
2.5 Object.fromEntries() - из массива пар
javascript
const entries = [["name", "Анна"], ["age", 25]];
const obj = Object.fromEntries(entries); // { name: "Анна", age: 25 }
Часть 3. Вычисляемые свойства (динамические ключи)
Одна из самых мощных фич - ключи, которые вычисляются во время выполнения.
javascript
const dynamicKey = "status";
const obj = {
name: "Анна",
[dynamicKey]: "active", // ключ будет "status"
[`get${dynamicKey}`]() { // метод "getstatus"
return this[dynamicKey];
}
};
console.log(obj.status); // "active"
console.log(obj.getstatus()); // "active"
Часть 4. Дескрипторы свойств: Невидимая власть
У каждого свойства объекта есть три флага:
- writable - можно ли изменить значение
- enumerable - показывается ли в циклах (for...in, Object.keys)
- configurable - можно ли удалить свойство или изменить дескрипторы
javascript
const user = { name: "Анна" };
// Получить дескриптор
const descriptor = Object.getOwnPropertyDescriptor(user, "name");
console.log(descriptor);
// { value: "Анна", writable: true, enumerable: true, configurable: true }
// Изменить дескриптор
Object.defineProperty(user, "name", {
writable: false, // теперь свойство "только для чтения"
enumerable: false // не показывается в циклах
});
user.name = "Борис"; // Ошибка в строгом режиме (тихо игнорируется в нестрогом)
console.log(user.name); // "Анна" (не изменилось!)
4.1 Создание скрытых свойств (enumerable: false)
javascript
const obj = { visible: 1 };
Object.defineProperty(obj, "secret", {
value: "тайна",
enumerable: false // не появится в Object.keys()
});
console.log(Object.keys(obj)); // ["visible"]
console.log(obj.secret); // "тайна" (но доступ есть)
4.2 Заморозка объектов
javascript
const obj = { name: "Анна" };
Object.freeze(obj); // делает все свойства неизменяемыми
obj.name = "Борис"; // игнорируется (или ошибка в strict mode)
delete obj.name; // игнорируется
console.log(obj.name); // "Анна"
// Проверка
console.log(Object.isFrozen(obj)); // true
4.3 Запечатывание (seal) - нельзя добавлять/удалять, но можно изменять
javascript
const obj = { name: "Анна", age: 25 };
Object.seal(obj);
obj.age = 26; // работает
obj.city = "Москва"; // игнорируется
delete obj.name; // игнорируется
console.log(obj); // { name: "Анна", age: 26 }
Часть 5. Геттеры и сеттеры: Вычисления при доступе
Вы можете перехватить чтение и запись свойства:
javascript
const user = {
firstName: "Анна",
lastName: "Иванова",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
}
};
console.log(user.fullName); // "Анна Иванова" (вызывается геттер)
user.fullName = "Борис Петров"; // вызывается сеттер
console.log(user.firstName); // "Борис"
5.1 Валидация через сеттер
javascript
const user = {
_age: 0,
get age() {
return this._age;
},
set age(value) {
if (value < 0) throw new Error("Возраст не может быть отрицательным");
if (value > 150) throw new Error("Слишком старый");
this._age = value;
}
};
user.age = 25; // OK
user.age = -5; // Ошибка!
Часть 6. Прототипы: Тайное наследование
У каждого объекта есть скрытая ссылка [[Prototype]] (доступна через __proto__ или Object.getPrototypeOf). Когда вы читаете свойство, JS сначала ищет его в самом объекте, потом в прототипе, потом в прототипе прототипа...
javascript
const parent = { name: "Родитель", greet() { return "Привет"; } };
const child = Object.create(parent);
child.age = 10;
console.log(child.age); // 10 (своё)
console.log(child.name); // "Родитель" (из прототипа)
console.log(child.greet()); // "Привет" (из прототипа)
// Проверка наличия свойства (своего, не из прототипа)
console.log(child.hasOwnProperty("age")); // true
console.log(child.hasOwnProperty("name")); // false
6.1 Цепочка прототипов
javascript
const animal = { eats: true };
const dog = Object.create(animal);
dog.barks = true;
const husky = Object.create(dog);
husky.color = "white";
console.log(husky.eats); // true (из animal)
console.log(husky.barks); // true (из dog)
console.log(husky.color); // "white" (своё)
// Вся цепочка
console.log(Object.getPrototypeOf(husky) === dog); // true
console.log(Object.getPrototypeOf(dog) === animal); // true
console.log(Object.getPrototypeOf(animal) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype)); // null
6.2 Создание объекта без прототипа
javascript
const cleanObject = Object.create(null);
cleanObject.name = "Анна";
console.log(cleanObject.toString); // undefined (нет метода)
console.log(cleanObject.hasOwnProperty); // undefined
// Идеально для словарей (чистых карт), где ключи - любые строки
const dictionary = Object.create(null);
dictionary["toString"] = "метод преобразования в строку";
// Нет конфликта с встроенным toString!
Часть 7. Итерация по объекту: Методы и подводные камни
7.1 for...in — перебирает СВОИ + НАСЛЕДОВАННЫЕ перечисляемые свойства
javascript
const parent = { inherited: "from parent" };
const child = Object.create(parent);
child.own = "from child";
for (let key in child) {
console.log(key); // "own", потом "inherited"
}
// Фильтруем только свои
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key); // только "own"
}
}
7.2 Object.keys() - только СВОИ перечисляемые (идеально)
javascript
const keys = Object.keys(child); // ["own"]
7.3 Object.values() - значения своих свойств
javascript
const values = Object.values(child); // ["from child"]
7.4 Object.entries() - пары [ключ, значение]
javascript
const entries = Object.entries(child); // [["own", "from child"]]
Часть 8. Копирование объектов: Глубокая и поверхностная
8.1 Поверхностное копирование (shallow copy)
javascript
const original = { a: 1, b: { c: 2 } };
// Способ 1: spread
const copy1 = { ...original };
// Способ 2: Object.assign
const copy2 = Object.assign({}, original);
// Способ 3: для массивов
const arrCopy = [...originalArray];
// Проблема: вложенные объекты копируются по ссылке
copy1.b.c = 3;
console.log(original.b.c); // 3 (изменилось!)
8.2 Глубокое копирование (deep copy)
javascript
const original = { a: 1, b: { c: 2 }, d: [1, 2, 3] };
// Способ 1: JSON (не работает с функциями, Date, undefined, циклическими ссылками)
const copy1 = JSON.parse(JSON.stringify(original));
// Способ 2: structuredClone (современный браузерный API)
const copy2 = structuredClone(original);
// Способ 3: ручная рекурсия (для сложных случаев)
function deepCopy(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== "object") return obj;
if (hash.has(obj)) return hash.get(obj);
const copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], hash);
}
}
return copy;
}
Часть 9. Сравнение объектов: По ссылке, не по значению
javascript
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;
console.log(obj1 === obj2); // false (разные объекты)
console.log(obj1 === obj3); // true (один и тот же объект)
// Как сравнить по содержимому?
function shallowEqual(objA, objB) {
if (objA === objB) return true;
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => objA[key] === objB[key]);
}
console.log(shallowEqual(obj1, obj2)); // true
Часть 10. Object методы, которые нужно знать
Часть 11. Реальные паттерны с объектами
11.1 Объект как словарь (Map без прототипа)
javascript
const cache = Object.create(null);
cache["user:1"] = { name: "Анна" };
cache["toString"] = "это не метод";
console.log(cache["toString"]); // "это не метод" (нет конфликта)
11.2 Конфигурация с валидацией через сеттеры
javascript
const config = {
_apiUrl: "",
_timeout: 5000,
get apiUrl() { return this._apiUrl; },
set apiUrl(value) {
if (!value.startsWith("https://")) {
throw new Error("API URL должен использовать HTTPS");
}
this._apiUrl = value;
},
get timeout() { return this._timeout; },
set timeout(value) {
if (value < 100) throw new Error("Таймаут не может быть меньше 100ms");
if (value > 30000) throw new Error("Таймаут не может быть больше 30000ms");
this._timeout = value;
}
};
config.apiUrl = "https://api.example.com"; // OK
// config.apiUrl = "http://api.example.com"; // Ошибка!
11.3 Создание неизменяемого объекта (глубоко)
javascript
function deepFreeze(obj) {
Object.freeze(obj);
for (let key in obj) {
if (obj.hasOwnProperty(key) && typeof obj[key] === "object") {
deepFreeze(obj[key]);
}
}
return obj;
}
const constants = deepFreeze({
API: {
URL: "https://api.example.com",
VERSION: "v2"
},
STATUS: {
ACTIVE: "active",
INACTIVE: "inactive"
}
});
constants.API.URL = "другое"; // Игнорируется (ошибка в strict mode)
Часть 12. Подводные камни
Камень #1: hasOwnProperty может быть переопределён
javascript
const obj = { hasOwnProperty: "не метод" };
console.log(obj.hasOwnProperty("name")); // TypeError!
// Решение
console.log(Object.prototype.hasOwnProperty.call(obj, "name"));
// Или современный способ
console.log(Object.hasOwn(obj, "name"));
Камень #2: Потеря this в методах
javascript
const user = {
name: "Анна",
greet() { return `Привет, ${this.name}`; }
};
const greet = user.greet;
console.log(greet()); // "Привет, undefined" (this = window)
// Решение: bind
const boundGreet = user.greet.bind(user);
Камень #3: Object.keys не гарантирует порядок
javascript
const obj = { 100: "сто", 2: "два", 1: "один" };
console.log(Object.keys(obj)); // ["1", "2", "100"] (сначала числа по порядку)
// Для гарантии порядка используйте Map или массивы пар
Итог: Манифест объектов
- Объекты - по ссылке - копирование создаёт новый объект, присваивание - ссылку.
- Ключи - строки или символы - числа превращаются в строки.
- Дескрипторы - контролируйте доступ через defineProperty.
- Прототипы - понимайте цепочку, используйте Object.create для чистых словарей.
- Итерация - Object.keys для своих свойств, for...in + hasOwnProperty для своих+наследуемых.
- Копирование - structuredClone для глубокого, {...obj} для поверхностного.
- Геттеры/сеттеры - для вычисляемых свойств и валидации.
Финальный тест (проверьте себя):
javascript
const a = { value: 1 };
const b = a;
b.value = 2;
console.log(a.value);
const c = { value: 1 };
const d = { ...c };
d.value = 2;
console.log(c.value);
const e = { x: { y: 1 } };
const f = { ...e };
f.x.y = 2;
console.log(e.x.y);
Ответы: 2 (ссылка), 1 (поверхностная копия), 2 (вложенный объект скопирован по ссылке).
Объекты - это фундамент JavaScript. Освойте их - и вы поймёте язык на 80%. Остальные 20% - это замыкания, прототипы и асинхронность. Но теперь вы знаете об объектах достаточно, чтобы называть себя экспертом.
------------------------------------------------------------------------------------
------------------------------------------------------------------------------------
Отражения и оригиналы: Правда о копировании объектов и ссылках в JavaScript
Вы когда-нибудь пытались скопировать объект, изменить копию и обнаруживали, что изменился оригинал? Или часами искали баг, который оказывался "магическим" изменением массива в другом конце кода?
Добро пожаловать в мир ссылок и копирования — место, где новички теряют часы, а профи знают шесть способов создания настоящей копии. Сегодня мы разберем, почему объекты ведут себя не как числа или строки, и как обуздать эту "ссылко-магию".
Часть 1. Примитивы против объектов: Битва типов
В JavaScript есть два лагеря: примитивы и объекты. Их поведение при присваивании отличается кардинально.
Примитивы копируются по значению
javascript
let a = 42;
let b = a; // b получил КОПИЮ значения 42
b = 100;
console.log(a); // 42 (не изменилось!)
console.log(b); // 100
Числа, строки, булевы значения, null, undefined, symbol, bigint ведут себя предсказуемо. Каждая переменная хранит свое собственное значение.
Объекты копируются по ссылке
javascript
const user1 = { name: "Анна" };
const user2 = user1; // user2 получил ССЫЛКУ на тот же объект
user2.name = "Борис";
console.log(user1.name); // "Борис" (оригинал изменился!)
console.log(user2.name); // "Борис"
Что произошло? Переменная user1 не хранит сам объект. Она хранит адрес в памяти, где этот объект лежит. При присваивании user2 = user1 мы копируем этот адрес. Теперь две переменные указывают на одну и ту же коробку в памяти.
Часть 2. Почему так сделано? (Спойлер: производительность)
Представьте, что объект - это огромный чемодан с вещами. Если бы JavaScript копировал его по значению каждый раз, код работал бы ужасно медленно.
javascript
const hugeObject = {
// миллион свойств
};
const copy = hugeObject; // Если бы копировалось по значению, это заняло бы секунды
Правда: Ссылки делают JavaScript быстрым и эффективным. Но иногда нам действительно нужна копия. И тогда начинается магия.
Часть 3. Ссылки на ссылки: Цепная реакция
Ссылки работают на любую глубину:
javascript
const obj1 = { data: 1 };
const obj2 = obj1;
const obj3 = obj2;
obj3.data = 999;
console.log(obj1.data); // 999 (изменилось всё!)
Все три переменные смотрят на один объект. Изменение через любую из них видно через все.
Часть 4. Поверхностное копирование (Shallow Copy)
Поверхностная копия создает новый объект, но вложенные объекты остаются общими.
Способ 1: Spread оператор {...}
javascript
const original = {
name: "Анна",
address: { city: "Москва", street: "Тверская" }
};
const copy = { ...original };
copy.name = "Борис"; // меняем копию
copy.address.city = "СПб"; // меняем вложенный объект
console.log(original.name); // "Анна" (не изменилось)
console.log(original.address.city); // "СПб" (изменилось!)
Проблема: address - это объект. Spread скопировал только ссылку на него.
Способ 2: Object.assign()
javascript
const copy = Object.assign({}, original);
// Работает так же, как spread
Способ 3: Для массивов - spread [...]
javascript
const originalArr = [1, 2, { deep: 3 }];
const copyArr = [...originalArr];
copyArr[0] = 999; // меняем примитив
copyArr[2].deep = 777; // меняем вложенный объект
console.log(originalArr[0]); // 1 (не изменилось)
console.log(originalArr[2].deep); // 777 (изменилось!)
Часть 5. Глубокое копирование (Deep Copy)
Здесь создается полностью независимая копия, включая все вложенные структуры.
Способ 1: JSON.parse(JSON.stringify()) - быстрый, но опасный
javascript
const original = {
name: "Анна",
address: { city: "Москва" },
date: new Date(),
func: () => console.log("hi"),
undef: undefined,
inf: Infinity
};
const copy = JSON.parse(JSON.stringify(original));
copy.address.city = "СПб";
console.log(original.address.city); // "Москва" (не изменилось!)
// ПРОБЛЕМЫ:
console.log(copy.date); // строка, а не Date!
console.log(copy.func); // undefined (функции потеряны)
console.log(copy.undef); // undefined (но свойство пропало)
console.log(copy.inf); // null (Infinity превращается в null)
Что теряется: функции, undefined, Symbol, Infinity, NaN, Date, RegExp, циклические ссылки.
Способ 2: structuredClone() - современный герой
javascript
const original = {
name: "Анна",
address: { city: "Москва" },
date: new Date(),
regex: /hello/g,
map: new Map([["key", "value"]]),
set: new Set([1, 2, 3])
};
const copy = structuredClone(original);
copy.address.city = "СПб";
console.log(original.address.city); // "Москва"
// structuredClone сохраняет:
console.log(copy.date instanceof Date); // true
console.log(copy.regex instanceof RegExp); // true
console.log(copy.map instanceof Map); // true
console.log(copy.set instanceof Set); // true
Что НЕ умеет: функции, DOM-узлы, некоторые встроенные объекты.
Способ 3: Ручная рекурсия (для сложных случаев)
javascript
function deepCopy(obj, hash = new WeakMap()) {
// Примитивы и null
if (obj === null || typeof obj !== "object") return obj;
// Защита от циклических ссылок
if (hash.has(obj)) return hash.get(obj);
// Создаём копию нужного типа
const copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy);
// Копируем все свойства (включая символы)
Reflect.ownKeys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], hash);
});
return copy;
}
// Тест с циклической ссылкой
const circular = { name: "Анна" };
circular.self = circular;
const copy = deepCopy(circular);
console.log(copy.self === copy); // true (цикл сохранён!)
Способ 4: Библиотеки (lodash)
javascript
import _ from 'lodash';
const copy = _.cloneDeep(original);
// Надёжно, медленно, но проверено годами
Часть 6. Сравнение методов копирования
Часть 7. Копирование массивов
Массивы - это объекты, поэтому правила те же.
Поверхностное копирование
javascript
const original = [1, 2, [3, 4]];
// Способ 1: spread
const copy1 = [...original];
// Способ 2: slice
const copy2 = original.slice();
// Способ 3: Array.from
const copy3 = Array.from(original);
// Способ 4: concat
const copy4 = original.concat();
// Все они поверхностные!
copy1[2][0] = 999;
console.log(original[2][0]); // 999 (изменилось!)
Глубокое копирование массива
javascript
const original = [1, 2, [3, 4]];
// structuredClone
const deep = structuredClone(original);
deep[2][0] = 999;
console.log(original[2][0]); // 3 (не изменилось)
Часть 8. Копирование объектов с методами
javascript
const user = {
name: "Анна",
greet() {
return `Привет, ${this.name}`;
}
};
// spread копирует методы
const copy = { ...user };
console.log(copy.greet()); // "Привет, Анна" (работает!)
// А вот JSON.stringify потеряет
const bad = JSON.parse(JSON.stringify(user));
// bad.greet is not a function
Часть 9. Реальные сценарии
Сценарий 1: Состояние в React/Vue
javascript
// Плохо (мутация)
state.user.name = "Борис";
setState(state); // React может не заметить изменение
// Хорошо (новая копия)
setState({
...state,
user: {
...state.user,
name: "Борис"
}
});
Сценарий 2: Форма с глубокими данными
javascript
function Form({ initialData }) {
const [formData, setFormData] = useState(
() => structuredClone(initialData) // глубокая копия при монтировании
);
const handleChange = (field, value) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
// ...
}
Сценарий 3: Кэширование с защитой от мутации
javascript
class Cache {
constructor() {
this.store = new Map();
}
set(key, value) {
// Сохраняем копию, чтобы внешние изменения не испортили кэш
this.store.set(key, structuredClone(value));
}
get(key) {
// Возвращаем копию, чтобы вызывающий код не испортил кэш
const value = this.store.get(key);
return value ? structuredClone(value) : undefined;
}
}
Часть 10. Сравнение объектов после копирования
javascript
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
const obj3 = obj1;
console.log(obj1 === obj2); // false (разные объекты)
console.log(obj1 === obj3); // true (один и тот же)
// Как сравнить содержимое?
function isEqual(objA, objB) {
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => objA[key] === objB[key]);
}
console.log(isEqual(obj1, obj2)); // true (содержимое одинаково)
Часть 11. Подводные камни
Камень #1: Забыл про вложенность
javascript
const config = {
server: {
port: 3000,
host: "localhost"
}
};
// Думаем, что скопировали
const copy = { ...config };
copy.server.port = 8080;
console.log(config.server.port); // 8080 (сюрприз!)
Камень #2: Object.assign мутирует первый аргумент
javascript
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(target === result); // true (target изменён)
console.log(target); // { a: 1, b: 2 }
// Для создания копии используем пустой объект
const copy = Object.assign({}, source);
Камень #3: structuredClone не везде доступен
javascript
// В Node.js < 17 нужен флаг
// node --experimental-global-webcrypto script.js
// Полифил для старых браузеров
if (!window.structuredClone) {
window.structuredClone = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
}
Часть 12. Чек-лист: Какую копию выбрать?
Итог: Манифест копирования
- Примитивы копируются по значению - безопасны.
- Объекты копируются по ссылке - мутация через любую ссылку меняет оригинал.
- Поверхностное копирование - {...obj} - копирует только первый уровень.
- Глубокое копирование - structuredClone - полная независимость.
- JSON метод - быстр, но теряет функции, Date, undefined, Symbol.
- Всегда думайте о вложенности - самый частый источник багов.
- Иммутабельность - ключ к предсказуемому коду (React, Redux, Vue).
Финальный тест (проверьте себя):
javascript
const original = {
name: "Анна",
scores: [10, 20, 30],
meta: { level: 5 }
};
const copy1 = original;
const copy2 = { ...original };
const copy3 = JSON.parse(JSON.stringify(original));
const copy4 = structuredClone(original);
copy1.name = "1";
copy2.name = "2";
copy3.name = "3";
copy4.name = "4";
copy1.scores.push(40);
copy2.scores.push(50);
copy3.scores.push(60);
copy4.scores.push(70);
console.log(original.name); // ?
console.log(original.scores); // ?
Ответ:
- original.name - "1" (copy1 изменил оригинал)
- original.scores - [10, 20, 30, 40, 50] (copy1 и copy2 изменили один и тот же массив, потому что scores копировался поверхностно)
Копирование объектов - это та область JavaScript, где теория расходится с практикой чаще всего. Но когда вы поймете разницу между ссылкой, поверхностной и глубокой копией, вы перестанете удивляться "магическим" изменениям данных. Помните: в мире объектов нет магии, есть только ссылки и копии. Выбирайте правильную стратегию, и ваш код будет предсказуемым и надёжным.
---------------------------------------------------------------------------------
---------------------------------------------------------------------------------
Невидимые уборщики: Как работает сборка мусора в JavaScript (и почему ваша память не взрывается)
Вы пишете код. Создаёте объекты, массивы, функции. И никогда не думаете о том, чтобы их удалить. В C++ вы бы вызывали delete. В Rust - боролись бы с borrow checker. В JavaScript же... просто ничего не делаете.
И память не течёт. Ну, почти никогда.
Как так получается? За кулисами трудится невидимый уборщик - сборщик мусора (Garbage Collector). Он ходит по вашему хип-хопу памяти, выискивает мусор и выбрасывает его. Без вопросов. Без просьб. Без благодарности.
Сегодня мы поднимем капот JavaScript-движка и посмотрим, как работает этот магический механизм. И главное - как не заставить его работать слишком усердно.
Часть 1. Зачем вообще нужна сборка мусора?
В JavaScript память выделяется автоматически:
javascript
// Выделяем память под объект
const user = { name: "Анна", age: 25 };
// Выделяем память под массив
const data = new Array(1000000);
// Выделяем память под функцию
const greet = () => console.log("Привет");
Вопрос: когда эту память нужно освобождать? Когда объект больше не нужен. Но откуда программа знает, что объект "не нужен"? В этом и заключается задача сборщика мусора.
Основная идея: объект жив, пока до него можно добраться из корневых узлов (глобальный объект, текущие локальные переменные, стек вызовов). Как только объект становится недостижимым - он мусор.
Часть 2. Достижимость: Кто живёт, а кто нет?
Представьте, что память - это комната, а корневые узлы - это люди, стоящие у входа. Любой объект, до которого можно дойти по цепочке "человек → объект → другой объект", остаётся в комнате. Все остальные - выносятся.
javascript
// Глобальная переменная (корневой узел)
let globalUser = { name: "Анна" };
function start() {
// Локальная переменная (тоже корневой узел, пока функция выполняется)
let localUser = { name: "Борис" };
// globalUser и localUser достижимы → живут
// Объект { name: "Анна" } жив, пока globalUser на него ссылается
// Объект { name: "Борис" } жив, пока выполняется start()
}
start(); // После вызова localUser становится недостижимым → мусор
Часть 3. Алгоритмы сборки мусора
Сборщиков мусора не один, а несколько. Движки комбинируют их для оптимальной производительности.
3.1 Алгоритм "Пометить и очистить" (Mark-and-Sweep)
Это основа современной сборки мусора.
Шаг 1: Mark (Пометка) - сборщик проходит от корней и помечает все достижимые объекты.
javascript
let a = { name: "A" };
let b = { name: "B" };
a.child = b;
b.child = a; // циклическая ссылка!
a = null; // a больше не ссылается на объект
// Алгоритм:
// 1. Старт от корней (глобальный объект)
// 2. Помечает достижимые объекты
// 3. Объект { name: "A" } недостижим → не помечен
// 4. Объект { name: "B" } тоже недостижим (через a, но a = null)
// 5. Оба отправляются в Sweep
Шаг 2: Sweep (Очистка) - удаляются все непомеченные объекты.
Важно: Алгоритм работает с циклическими ссылками! Раньше (в старых браузерах) была проблема с циклами:
javascript
// Старая проблема (до 2012 года)
function createCycle() {
const obj1 = {};
const obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;
return obj1;
}
let cycle = createCycle();
cycle = null;
// Раньше: объекты не удалялись (счётчик ссылок не обнулялся)
// Сейчас: Mark-and-Sweep помечает только достижимые → удаляет оба
3.2 Алгоритм "Копирование" (Copying)
Делит память на две полу-области: "from-space" и "to-space". Живые объекты копируются из from в to, после чего from целиком очищается.
Плюсы: быстро, без фрагментации памяти.
Минусы: использует в два раза больше памяти.
3.3 Алгоритм "Generational" (Поколенческий)
Основан на наблюдении: большинство объектов живут недолго (создаются и сразу умирают).
Память делится на поколения:
- New space (молодое поколение) - новые объекты. Очищается часто и быстро.
- Old space (старое поколение) - объекты, пережившие несколько сборок. Очищается редко.
javascript
// Пример долгоживущих объектов
const cache = {}; // живёт долго (попадёт в old space)
for (let i = 0; i < 1000; i++) {
const temp = { id: i }; // живёт одну итерацию → young space
process(temp);
}
3.4 Алгоритм "Incremental" (Инкрементальный)
Вместо одной долгой остановки (Stop-The-World), сборщик работает по кусочкам между выполнением кода. Так страница не зависает.
javascript
// Старый подход (Stop-The-World)
// Пауза 100ms → страница зависает
// Новый подход (Incremental)
// 10 пауз по 10ms → почти незаметно
Часть 4. V8: Движок Chrome и Node.js
V8 использует комбинацию алгоритмов:
ПоколениеАлгоритмЧто делаетYoung generationScavenge (копирование)Быстрая сборка мелкого мусораOld generationMark-Sweep + Mark-CompactПолная очистка + дефрагментация
4.1 Как увидеть сборку мусора в Chrome DevTools
javascript
// 1. Откройте Performance Monitor (Ctrl+Shift+P → Show Performance Monitor)
// 2. Смотрите график "JS heap size"
// 3. Создайте много объектов
const leak = [];
setInterval(() => {
leak.push(new Array(1000000));
console.log("Heap size растёт");
}, 1000);
4.2 Включение отладочного вывода в Node.js
bash
node --trace-gc script.js
node --trace-gc-verbose script.js
Часть 5. Утечки памяти: Когда уборщик не справляется
Сборщик мусора - не волшебник. Он удаляет только недостижимые объекты. Если объект случайно остаётся достижимым - он не будет удалён никогда. Это называется утечкой памяти.
5.1 Глобальные переменные
javascript
function leak() {
// Забыли var/let/const
accidentallyGlobal = new Array(1000000);
}
leak(); // переменная попала в window → живёт вечно
Решение: используйте "use strict", который запрещает необъявленные переменные.
5.2 Забытые таймеры
javascript
let data = new Array(1000000);
setInterval(() => {
console.log(data.length); // data никогда не освободится
}, 1000);
// data нужна, пока жив таймер
// если таймер не отменить - data живёт вечно
Решение: всегда очищайте таймеры.
javascript
const timer = setInterval(() => {}, 1000);
clearInterval(timer); // теперь data может быть собрана
5.3 Слушатели событий
javascript
function createHandler() {
const hugeData = new Array(1000000);
document.getElementById("btn").addEventListener("click", () => {
console.log(hugeData.length);
});
}
createHandler();
// hugeData живёт, пока жив обработчик
// обработчик живёт, пока жива кнопка
Решение: удаляйте слушатели, когда они больше не нужны.
javascript
const handler = () => console.log("click");
button.addEventListener("click", handler);
button.removeEventListener("click", handler);
5.4 Замыкания
javascript
function outer() {
const huge = new Array(1000000);
return function inner() {
// inner имеет доступ к huge
console.log("hello");
// huge не используется, но всё ещё в замыкании!
};
}
const fn = outer();
fn(); // huge всё ещё в памяти (хотя не используется)
Решение: современные движки оптимизируют такие случаи, но лучше не полагаться.
5.5 Отсоединённые DOM-элементы
javascript
const element = document.createElement("div");
document.body.appendChild(element);
element.remove(); // удалили из DOM
// НО! element всё ещё в переменной
console.log(element); // живёт в памяти
Часть 6. Как помочь сборщику мусора (и не мешать)
6.1 Обнуляйте ссылки
javascript
// Плохо (долгая жизнь)
let cache = { user: hugeObject };
// ... используем cache
// Хорошо (явно говорим, что больше не нужен)
let cache = { user: hugeObject };
// ... используем cache
cache = null; // теперь hugeObject может быть собран
6.2 Используйте слабые ссылки (WeakMap, WeakSet, WeakRef)
Обычные ссылки не дают объекту умереть. Слабые - позволяют.
javascript
// Обычный Map - объект живёт, пока есть ключ
let user = { name: "Анна" };
const cache = new Map();
cache.set(user, "данные");
user = null;
// { name: "Анна" } всё ещё в cache → живёт!
// WeakMap - объект умрёт, когда не останется других ссылок
let user = { name: "Анна" };
const cache = new WeakMap();
cache.set(user, "данные");
user = null;
// Объект может быть собран, даже если есть WeakMap
6.3 Ограничивайте размер кэша
javascript
class LRUCache {
constructor(limit) {
this.limit = limit;
this.cache = new Map();
}
set(key, value) {
if (this.cache.size >= this.limit) {
// удаляем самый старый элемент
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
}
6.4 Используйте пулы объектов для тяжёлых структур
javascript
class ObjectPool {
constructor(createFn, maxSize = 100) {
this.createFn = createFn;
this.pool = [];
this.maxSize = maxSize;
}
acquire() {
return this.pool.pop() || this.createFn();
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.pool.push(obj);
}
}
}
// Вместо создания миллиона объектов
const pool = new ObjectPool(() => ({}));
const obj = pool.acquire();
// используем obj
pool.release(obj);
Часть 7. Инструменты для поиска утечек
7.1 Chrome DevTools: Memory Tab
- Heap snapshot - снимок памяти. Сделайте два снимка и сравните.
- Allocation timeline - запись выделений в реальном времени.
- Allocation sampling - семплирование (меньше влияет на производительность).
javascript
// Как искать утечку:
// 1. Сделайте Heap Snapshot (память до)
// 2. Выполните действие, которое подозреваете в утечке
// 3. Сделайте Heap Snapshot (память после)
// 4. Сравните (вкладка Comparison)
// 5. Ищите объекты, которые не должны были остаться
7.2 Node.js: process.memoryUsage()
javascript
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: `${Math.round(usage.rss / 1024 / 1024)} MB`, // общая память
heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`, // выделено под JS
heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`, // используется
external: `${Math.round(usage.external / 1024 / 1024)} MB` // C++ объекты
});
}, 5000);
7.3 Node.js: inspector
bash
node --inspect script.js
# Открыть chrome://inspect
Часть 8. Мифы о сборке мусора
Миф 1: "delete освобождает память"
javascript
const obj = { a: 1, b: 2 };
delete obj.a; // удаляет свойство, но не освобождает память
Миф 2: "null освобождает память"
javascript
let obj = { data: hugeArray };
obj = null; // теперь hugeArray может быть собран (но не сразу!)
Миф 3: "Сборщик мусора тормозит всегда"
Современные инкрементальные и параллельные алгоритмы почти незаметны.
Миф 4: "В JavaScript нет утечек памяти"
Есть. И их много.
Часть 9. Реальный кейс: Утечка в React-компоненте
jsx
function LeakyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const huge = new Array(1000000);
setData(huge);
// ПРОБЛЕМА: данные никогда не очищаются
// даже когда компонент размонтирован
}, []);
return <div>...</div>;
}
// Исправление:
function FixedComponent() {
const [data, setData] = useState(null);
useEffect(() => {
const huge = new Array(1000000);
setData(huge);
return () => {
// Очищаем при размонтировании
setData(null);
};
}, []);
return <div>...</div>;
}
Итог: Манифест уборщика
- Сборка мусора автоматическая - не вызывайте её вручную (даже если очень хочется).
- Mark-and-Sweep - основа современных алгоритмов (работает с циклами).
- Поколенческая сборка - новые объекты собираются чаще, старые — реже.
- Утечки памяти реальны - глобальные переменные, забытые таймеры, слушатели, замыкания.
- WeakMap/WeakSet - ваши друзья для кэширования без утечек.
- DevTools Memory Tab - лучший способ найти утечку.
- Чистите за собой - обнуляйте ссылки, удаляйте слушатели, отменяйте таймеры.
Финальный тест: что произойдёт с памятью?
javascript
let arr = new Array(1000000);
let ref = arr;
arr = null;
// Через 5 секунд сборщик мусора запустится
// Будет ли массив удалён?
Ответ: Нет. ref всё ещё ссылается на массив. Пока есть хотя бы одна ссылка, объект живёт.
Сборщик мусора - это тихий герой JavaScript. Вы редко замечаете его работу, но без него браузеры бы падали через минуту работы. Понимание того, как он работает, помогает писать код, который не течёт, не тормозит и не взрывает память. Уважайте своего невидимого уборщика, и он отплатит вам стабильной и быстрой работой приложений.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Зеркальный лабиринт: Полное руководство по this и методам объектов в JavaScript
this - это самое загадочное существо в JavaScript. Он меняется в зависимости от того, кто его позвал, где он стоит и в какой фазе луны. Новички его боятся. Профи используют его силу. А некоторые просто всегда пишут стрелочные функции и делают вид, что проблемы не существует.
Но this - это не баг. Это фича. Очень мощная фича. Просто у неё сложные правила.
Сегодня мы разберем методы объектов, тайны this и научимся управлять этим неуловимым контекстом.
Часть 1. Что такое метод объекта?
Метод - это функция, которая является свойством объекта.
javascript
const user = {
name: "Анна",
// Метод (сокращённая запись)
greet() {
console.log(`Привет, меня зовут ${this.name}`);
},
// Тоже метод (классическая запись)
sayGoodbye: function() {
console.log(`Пока, говорит ${this.name}`);
}
};
user.greet(); // "Привет, меня зовут Анна"
user.sayGoodbye(); // "Пока, говорит Анна"
Магия здесь в this. Внутри метода this указывает на объект, которому принадлежит метод.
Часть 2. this в разных контекстах (и почему он такой непредсказуемый)
2.1 Глобальный контекст
javascript
console.log(this); // window (в браузере) или global (в Node.js)
// В строгом режиме
"use strict";
console.log(this); // undefined (внутри функции)
2.2 Метод объекта (обычная функция)
javascript
const obj = {
name: "Объект",
method() {
console.log(this.name);
}
};
obj.method(); // "Объект" (this = obj)
2.3 Обычная функция (не метод)
javascript
function regular() {
console.log(this);
}
regular(); // window (в нестрогом) или undefined (в строгом)
2.4 Стрелочная функция
У стрелочных функций НЕТ своего this. Они берут его из внешнего контекста.
javascript
const obj = {
name: "Анна",
regularMethod() {
const arrow = () => {
console.log(this.name); // this берётся из regularMethod
};
arrow();
},
arrowMethod: () => {
console.log(this.name); // this из глобального контекста!
}
};
obj.regularMethod(); // "Анна" (работает)
obj.arrowMethod(); // undefined (не работает)
Часть 3. Главная ловушка: Потеря this
Самая частая проблема - this теряется, когда метод передаётся как колбэк.
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
// Работает
user.greet(); // "Привет, Анна"
// НЕ РАБОТАЕТ!
const greetFunc = user.greet;
greetFunc(); // "Привет, undefined" (this = window)
// НЕ РАБОТАЕТ!
setTimeout(user.greet, 1000); // "Привет, undefined"
Что произошло? Когда мы пишем user.greet, мы берём функцию. Но когда мы вызываем greetFunc(), связь с объектом теряется. Функция вызывается сама по себе, и this становится глобальным объектом.
Часть 4. Способы сохранить this
4.1 Стрелочная функция (сохраняет контекст)
javascript
const user = {
name: "Анна",
greet() {
setTimeout(() => {
console.log(`Привет, ${this.name}`); // this = user
}, 1000);
}
};
user.greet(); // "Привет, Анна" (через секунду)
4.2 .bind() - привязать навсегда
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
const boundGreet = user.greet.bind(user);
boundGreet(); // "Привет, Анна"
setTimeout(user.greet.bind(user), 1000); // "Привет, Анна"
.bind() создаёт новую функцию, у которой this навсегда привязан к указанному объекту.
4.3 .call() и .apply() - вызвать с нужным this
javascript
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const user = { name: "Анна" };
// call — аргументы через запятую
greet.call(user, "Привет", "!"); // "Привет, Анна!"
// apply — аргументы массивом
greet.apply(user, ["Здравствуй", "..."]); // "Здравствуй, Анна..."
4.4 Сохранить в переменную (замыкание)
javascript
const user = {
name: "Анна",
greet() {
console.log(`Привет, ${this.name}`);
}
};
const self = user; // сохраняем ссылку
setTimeout(() => {
self.greet(); // работает
}, 1000);
Часть 5. Методы объекта: Синтаксисы и нюансы
5.1 Три способа создать метод
javascript
const obj = {
// Способ 1: сокращённый (современный)
method1() {
console.log("method1", this);
},
// Способ 2: function expression
method2: function() {
console.log("method2", this);
},
// Способ 3: стрелочная (НЕ РЕКОМЕНДУЕТСЯ для методов)
method3: () => {
console.log("method3", this); // this не из obj!
}
};
5.2 Динамические имена методов (computed names)
javascript
const methodName = "greet";
const user = {
[methodName]() {
console.log("Привет!");
}
};
user.greet(); // "Привет!"
5.3 this в цепочке вызовов
javascript
const calculator = {
value: 0,
add(n) {
this.value += n;
return this; // возвращаем себя для цепочки
},
multiply(n) {
this.value *= n;
return this;
},
getValue() {
return this.value;
}
};
const result = calculator.add(5).multiply(2).add(3).getValue();
console.log(result); // (5 * 2) + 3 = 13
Часть 6. this в классах
javascript
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Привет, ${this.name}`);
}
// Стрелочный метод (сохраняет this)
greetArrow = () => {
console.log(`Привет, ${this.name}`);
}
}
const user = new User("Анна");
const greet = user.greet;
const greetArrow = user.greetArrow;
greet(); // "Привет, undefined" (потеряли)
greetArrow(); // "Привет, Анна" (стрелка сохранила)
Часть 7. Реальные паттерны с this
7.1 Привязка обработчиков событий
javascript
class Button {
constructor(text) {
this.text = text;
this.element = document.createElement("button");
this.element.textContent = text;
// Вариант 1: bind
this.element.onclick = this.handleClick.bind(this);
// Вариант 2: стрелка
this.element.onclick = () => this.handleClick();
// Вариант 3: обёртка
this.element.onclick = (e) => this.handleClick(e);
}
handleClick(event) {
console.log(`Кнопка "${this.text}" нажата`);
}
}
7.2 Декоратор логирования
javascript
function logMethod(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = function(...args) {
console.log(`Вызов ${name} с аргументами:`, args);
const result = original.apply(this, args);
console.log(`Результат:`, result);
return result;
};
return descriptor;
}
class Calculator {
@logMethod
add(a, b) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// Вызов add с аргументами: [2, 3]
// Результат: 5
7.3 Сборщик событий (Event Bus)
javascript
class EventBus {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
}
emit(event, data) {
if (!this.listeners[event]) return;
this.listeners[event].forEach(callback => {
callback.call(this, data); // сохраняем контекст
});
}
off(event, callback) {
if (!this.listeners[event]) return;
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
}
const bus = new EventBus();
bus.on("user-login", function(user) {
console.log(`${user.name} вошёл в систему`);
console.log(this); // EventBus (благодаря .call)
});
Часть 8. Сравнение способов привязки this
Часть 9. Подводные камни
Камень #1: this в setTimeout
javascript
const obj = {
name: "Анна",
greet() {
setTimeout(function() {
console.log(this.name); // undefined!
}, 1000);
}
};
// Исправление:
setTimeout(() => console.log(this.name), 1000); // стрелка
setTimeout(this.greet.bind(this), 1000); // bind
Камень #2: this в деструктуризации
javascript
const user = {
name: "Анна",
greet() {
console.log(this.name);
}
};
const { greet } = user;
greet(); // "undefined" (потеряли this)
Камень #3: this в прототипах
javascript
function User(name) {
this.name = name;
}
User.prototype.greet = function() {
console.log(this.name);
};
const user = new User("Анна");
const greet = user.greet;
greet(); // undefined (потеряли this)
// Но user.greet() работает
Камень #4: this в конструкторе при возврате объекта
javascript
function User(name) {
this.name = name;
return { other: "object" }; // возвращаем другой объект
}
const user = new User("Анна");
console.log(user.name); // undefined (this потерян)
console.log(user.other); // "object"
Часть 10. Реальный кейс: Vue.js и React
Vue.js
javascript
new Vue({
data() {
return {
name: "Анна"
};
},
methods: {
// this указывает на экземпляр Vue
greet() {
console.log(this.name); // "Анна"
setTimeout(() => {
console.log(this.name); // стрелка сохраняет this
}, 1000);
}
}
});
React (классовый компонент)
jsx
class Button extends React.Component {
constructor(props) {
super(props);
// Нужно привязать методы
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this.props.label);
}
// Или использовать стрелку (экспериментально)
handleClick = () => {
console.log(this.props.label);
}
render() {
return <button onClick={this.handleClick}>Click</button>;
}
}
Часть 11. Правила this (шпаргалка)
Итог: Манифест this
- Значение this определяется в момент вызова, а не в момент создания (кроме стрелок).
- Стрелочные функции - самый простой способ сохранить this (но не для методов объектов).
- .bind() - создаёт функцию с навсегда привязанным this.
- .call() / .apply() - вызывают функцию с заданным this (один раз).
- В методах объектов используйте обычные функции (не стрелки), если хотите иметь доступ к объекту через this.
- В колбэках всегда проверяйте, не потерялся ли this (используйте стрелку, bind или сохраните в переменную).
- Классы - метод, переданный как колбэк, теряет this (привязывайте в конструкторе или используйте стрелки).
Финальный тест (что выведет?):
javascript
const obj = {
name: "Анна",
greet() { console.log(this.name); },
greetArrow: () => { console.log(this.name); }
};
const fn = obj.greet;
const fnArrow = obj.greetArrow;
fn();
fnArrow();
setTimeout(obj.greet, 100);
setTimeout(() => obj.greet(), 100);
Ответ:
- fn() - undefined (потеряли this)
- fnArrow() - undefined (стрелка берёт глобальный this)
- setTimeout(obj.greet, 100) - undefined (потеряли)
- setTimeout(() => obj.greet(), 100) - "Анна" (стрелка сохранила контекст)
this в JavaScript - это не проклятие. Это просто другой способ мышления. Как только вы поймёте, что значение this определяется тем, как вызвана функция, а не тем, где она написана, вы перестанете бояться. А когда освоите bind, call и apply - начнёте использовать эту силу в своих интересах. Помните: джедаи не боятся this. Они его контролируют.
-------------------------------------------------------------------------
-------------------------------------------------------------------------
Фабрика миров: Как работает конструктор и оператор new в JavaScript
Вы когда-нибудь задумывались, что происходит за кулисами, когда вы пишете new Array() или new Date()? Или почему в JavaScript нет классов (ну, до ES6 не было), но мы всё равно можем создавать множество однотипных объектов?
Добро пожаловать в мир конструкторов - фабрик, которые штампуют объекты по чертежу. Это одна из самых misunderstood (недопонятых) тем в JavaScript, но после этой статьи вы будете создавать объекты как заправский фабрикант.
Часть 1. Почему не просто объектный литерал?
Допустим, нам нужно создать несколько пользователей:
javascript
// Способ 1: объектный литерал (ручное производство)
const user1 = { name: "Анна", age: 25 };
const user2 = { name: "Борис", age: 30 };
const user3 = { name: "Вика", age: 28 };
// Уже на третьем пользователе хочется плакать
Проблемы ручного подхода:
- Дублирование кода - каждый раз пишем одно и то же
- Ошибки - легко пропустить свойство
- Изменения - нужно менять в 100 местах
- Отсутствие типобезопасности - нет гарантии, что объект правильный
Решение: конструктор + оператор new.
Часть 2. Оператор new: Что он на самом деле делает?
new - это волшебная палочка, которая превращает обычную функцию в фабрику объектов.
javascript
function User(name, age) {
this.name = name;
this.age = age;
}
const user = new User("Анна", 25);
console.log(user); // User { name: "Анна", age: 25 }
Что происходит внутри, когда вы пишете new User()?
javascript
// То, что вы пишете:
const user = new User("Анна", 25);
// То, что делает движок (упрощённо):
function newOperator(Constructor, ...args) {
// 1. Создаёт пустой объект
const obj = {};
// 2. Устанавливает прототип (связывает с Constructor.prototype)
Object.setPrototypeOf(obj, Constructor.prototype);
// 3. Вызывает Constructor с this = obj и переданными аргументами
const result = Constructor.apply(obj, args);
// 4. Возвращает obj (если Constructor не вернул объект)
return (result && typeof result === "object") ? result : obj;
}
То есть new делает четыре вещи:
- 🏗️ Создаёт новый пустой объект
- 🔗 Связывает его с прототипом конструктора
- 🎯 Вызывает конструктор с this, указывающим на новый объект
- 🎁 Возвращает новый объект (или то, что вернул конструктор, если это объект)
Часть 3. Прототипы и конструкторы: Неразлучная пара
Каждая функция в JavaScript имеет свойство prototype. Когда мы создаём объект через new, этот объект получает ссылку на Constructor.prototype.
javascript
function User(name) {
this.name = name;
}
// Добавляем метод в прототип
User.prototype.greet = function() {
console.log(`Привет, ${this.name}!`);
};
const user = new User("Анна");
user.greet(); // "Привет, Анна!"
// Проверяем связь
console.log(Object.getPrototypeOf(user) === User.prototype); // true
Важно: Методы, добавленные в prototype, разделяются между всеми экземплярами. Это экономит память.
javascript
// Плохо (каждый объект получает свою копию метода)
function BadUser(name) {
this.name = name;
this.greet = function() {
console.log(`Привет, ${this.name}`);
};
}
// Хорошо (метод общий для всех)
function GoodUser(name) {
this.name = name;
}
GoodUser.prototype.greet = function() {
console.log(`Привет, ${this.name}`);
};
const user1 = new GoodUser("Анна");
const user2 = new GoodUser("Борис");
console.log(user1.greet === user2.greet); // true (один и тот же метод)
Часть 4. Конструкторы с классами ES6 (синтаксический сахар)
ES6 ввел классы, но под капотом всё тот же прототипный механизм.
javascript
// Класс (новый синтаксис)
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Привет, ${this.name}`);
}
static createAnonymous() {
return new User("Гость", 0);
}
}
// То же самое без класса (старый синтаксис)
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.greet = function() {
console.log(`Привет, ${this.name}`);
};
User.createAnonymous = function() {
return new User("Гость", 0);
};
Часть 5. Возвращаемое значение конструктора
Обычно конструктор возвращает this (новый объект). Но можно вернуть что-то другое.
javascript
// Стандартное поведение
function Standard(name) {
this.name = name;
// неявно возвращает this
}
// Возврат примитива (игнорируется)
function PrimitiveReturn(name) {
this.name = name;
return 42; // примитив → игнорируется, вернётся this
}
// Возврат объекта (заменяет this)
function ObjectReturn(name) {
this.name = name; // этот код выполнится, но this не вернётся
return { other: "object" }; // вернётся этот объект
}
const p = new PrimitiveReturn("Анна");
console.log(p); // PrimitiveReturn { name: "Анна" }
const o = new ObjectReturn("Анна");
console.log(o); // { other: "object" }
console.log(o.name); // undefined (не тот объект)
Практическое применение: синглтон (один экземпляр на всю программу).
javascript
let instance = null;
function Database() {
if (instance) {
return instance;
}
this.connection = "connected";
instance = this;
return instance;
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true (один и тот же объект)
Часть 6. instanceof: Проверка родословной
Оператор instanceof проверяет, был ли объект создан через определённый конструктор.
javascript
function User(name) { this.name = name; }
function Admin(name) { this.name = name; }
const user = new User("Анна");
console.log(user instanceof User); // true
console.log(user instanceof Object); // true (все объекты наследники Object)
console.log(user instanceof Admin); // false
Как работает instanceof: идёт по цепочке прототипов и проверяет, есть ли там Constructor.prototype.
javascript
// Ручная реализация
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
Часть 7. Реальные паттерны конструкторов
7.1 Фабрика с валидацией
javascript
function Product(name, price) {
// Валидация
if (!name || name.trim() === "") {
throw new Error("Название товара обязательно");
}
if (price < 0) {
throw new Error("Цена не может быть отрицательной");
}
// Нормализация
this.name = name.trim();
this.price = price;
this.createdAt = new Date();
}
// Добавляем методы в прототип
Product.prototype.getDiscountedPrice = function(discountPercent) {
return this.price * (1 - discountPercent / 100);
};
Product.prototype.format = function() {
return `${this.name}: ${this.price} руб.`;
};
const laptop = new Product("Ноутбук", 50000);
console.log(laptop.format()); // "Ноутбук: 50000 руб."
console.log(laptop.getDiscountedPrice(10)); // 45000
7.2 Конструктор с приватными данными (через замыкание)
javascript
function BankAccount(initialBalance) {
// Приватная переменная (недоступна снаружи)
let balance = initialBalance;
// Публичные методы (замыкают balance)
this.deposit = function(amount) {
if (amount > 0) {
balance += amount;
return true;
}
return false;
};
this.withdraw = function(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return true;
}
return false;
};
this.getBalance = function() {
return balance;
};
}
const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // 1300
console.log(account.balance); // undefined (приватно!)
7.3 Цепочка конструкторов (наследование)
javascript
// Базовый конструктор
function Animal(name) {
this.name = name;
this.isAlive = true;
}
Animal.prototype.breathe = function() {
console.log(`${this.name} дышит`);
};
// Конструктор-наследник
function Dog(name, breed) {
// Вызываем родительский конструктор
Animal.call(this, name);
this.breed = breed;
}
// Наследуем прототип
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Добавляем свой метод
Dog.prototype.bark = function() {
console.log(`${this.name} гавкает!`);
};
const rex = new Dog("Рекс", "Овчарка");
rex.breathe(); // "Рекс дышит"
rex.bark(); // "Рекс гавкает!"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
7.4 Конструктор с подсчётом экземпляров
javascript
function User(name) {
this.name = name;
User.instanceCount++;
}
User.instanceCount = 0; // статическое свойство
User.prototype.getInfo = function() {
return `${this.name} (пользователь ${this.constructor.instanceCount})`;
};
const user1 = new User("Анна");
const user2 = new User("Борис");
const user3 = new User("Вика");
console.log(User.instanceCount); // 3
console.log(user2.getInfo()); // "Борис (пользователь 3)"
Часть 8. Конструкторы для встроенных объектов
Даже встроенные объекты создаются через конструкторы.
javascript
// Число
const num = new Number(42);
console.log(typeof num); // "object" (не примитив!)
console.log(num.valueOf()); // 42
// Строка
const str = new String("hello");
console.log(typeof str); // "object"
// Массив
const arr = new Array(1, 2, 3);
console.log(arr); // [1, 2, 3]
// Объект
const obj = new Object();
obj.name = "Анна";
// Дата
const date = new Date();
// Регулярное выражение
const regex = new RegExp("\\d+", "g");
Но будьте осторожны: для чисел, строк и булевых значений обычно используют примитивы, а не конструкторы.
javascript
// Плохо (создаёт объект-обёртку)
const strObj = new String("hello");
if (strObj === "hello") { /* false! */ }
// Хорошо (примитив)
const strPrim = "hello";
Часть 9. Подводные камни
Камень #1: Забытый new
Это самая частая ошибка. Без new функция выполняется как обычная и создаёт глобальные переменные.
javascript
function User(name) {
this.name = name; // без new this = window
}
const user = User("Анна"); // забыли new
console.log(user); // undefined
console.log(window.name); // "Анна" (загрязнили глобальный объект!)
Защита от забытого new:
javascript
function User(name) {
// Проверка, вызвана ли функция через new
if (!(this instanceof User)) {
return new User(name);
}
this.name = name;
}
// Теперь оба варианта работают
const user1 = User("Анна"); // работает (благодаря проверке)
const user2 = new User("Борис"); // тоже работает
Камень #2: Стрелочные функции не могут быть конструкторами
javascript
const ArrowConstructor = (name) => {
this.name = name;
};
const user = new ArrowConstructor("Анна"); // TypeError!
Камень #3: this в конструкторе можно переопределить
javascript
function User(name) {
this.name = name;
return { different: "object" };
}
const user = new User("Анна");
console.log(user.name); // undefined (вернулся другой объект)
Часть 10. Современные альтернативы конструкторам
10.1 Фабричные функции (проще и безопаснее)
javascript
function createUser(name, age) {
// Нет new, нет this, нет prototype
return {
name,
age,
greet() {
console.log(`Привет, ${this.name}`);
}
};
}
const user = createUser("Анна", 25);
user.greet(); // "Привет, Анна"
10.2 Классы ES6 (стандарт сейчас)
javascript
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Привет, ${this.name}`);
}
}
10.3 Object.create() (прототипное наследование в чистом виде)
javascript
const userProto = {
greet() {
console.log(`Привет, ${this.name}`);
}
};
function createUser(name) {
const user = Object.create(userProto);
user.name = name;
return user;
}
const user = createUser("Анна");
user.greet(); // "Привет, Анна"
Часть 11. Производительность: Конструкторы vs Фабрики
javascript
// Конструктор (быстрее для множества объектов)
function UserConstructor(name) {
this.name = name;
}
UserConstructor.prototype.greet = function() {
console.log(this.name);
};
// Фабрика (медленнее, но проще)
function createUserFactory(name) {
return {
name,
greet() { console.log(this.name); }
};
}
// Тест производительности (100000 объектов)
// Конструктор: ~15ms
// Фабрика: ~25ms
// Разница есть, но для 99% случаев не важна
Итог: Манифест конструктора
- new создаёт объект, связывает прототип, вызывает конструктор, возвращает объект.
- Забытый new - классическая ошибка. Используйте защиту или фабрики.
- Методы лучше добавлять в prototype - экономия памяти.
- instanceof проверяет принадлежность к конструктору (по прототипу).
- Стрелочные функции не могут быть конструкторами.
- Классы ES6 - современный синтаксис для конструкторов.
- Фабричные функции - проще и безопаснее, но чуть медленнее.
Финальный тест (что выведет?):
javascript
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype.sayHi = function() {
console.log(this.name);
};
const rabbit = new Rabbit("Банни");
const rabbit2 = Rabbit("Банни");
console.log(rabbit);
console.log(rabbit2);
console.log(window.name);
Ответ:
- rabbit - Rabbit { name: "Банни" } (создан через new)
- rabbit2 - undefined (без new функция вернула undefined)
- window.name - "Банни" (без new this = window, загрязнили глобальный объект)
Конструкторы и оператор new - это фундаментальная часть JavaScript. Даже если вы используете классы ES6, понимание того, как работают конструкторы, помогает писать более эффективный и безопасный код. А умение создавать свои конструкторы открывает дверь в мир объектно-ориентированного программирования в JavaScript. Стройте свои фабрики миров с умом!
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
Мост над пропастью: Как опциональная цепочка ?. спасла миллионы строк кода
Вы когда-нибудь писали такой код?
javascript
const city = user && user.address && user.address.city;
Или, что ещё хуже:
javascript
const userName = data && data.profile && data.profile.personal && data.profile.personal.name;
Это называется "лестница из &&" или "пирамида проверок". Она защищает от ошибки Cannot read property 'city' of undefined, но выглядит ужасно. И вы пишете это снова, и снова, и снова.
Добро пожаложаловать в мир опциональной цепочки ?. - оператора, который делает эти проверки красивыми, короткими и безопасными.
Часть 1. Проблема: TypeError, который преследует каждого
javascript
const user = {
name: "Анна"
};
console.log(user.address.city); // 💥 TypeError: Cannot read property 'city' of undefined
Ваше приложение падает. Пользователь видит белый экран. Вы ищете, где address стал undefined. Знакомо?
Старое решение - проверки с &&:
javascript
const city = user && user.address && user.address.city;
console.log(city); // undefined (без ошибки)
Работает, но:
- Чем глубже вложенность, тем длиннее строка
- Легко пропустить одну проверку
- Код становится шумным
Часть 2. Опциональная цепочка: Кратчайший путь
Опциональная цепочка ?. позволяет читать свойство глубоко вложенного объекта без проверки каждого уровня. Если какая-то часть цепочки равна null или undefined, выражение возвращает undefined вместо ошибки.
javascript
const user = {
name: "Анна"
};
console.log(user?.address?.city); // undefined (без ошибки!)
Магия: ?. останавливает вычисление, как только встречает null или undefined.
Часть 3. Синтаксис: Три формы опциональной цепочки
3.1 obj?.prop - чтение свойства
javascript
const value = obj?.property;
// Работает как:
// const value = (obj === null || obj === undefined) ? undefined : obj.property;
3.2 obj?.[expr] - динамическое свойство
javascript
const key = "address";
const city = user?.[key]?.city;
3.3 obj?.method() - вызов метода
javascript
const result = user?.getAddress?.();
// Если user === null/undefined → undefined
// Если user.getAddress === null/undefined → undefined
// Иначе вызывает user.getAddress()
Часть 4. Глубокое погружение: Как это работает
javascript
const data = {
user: {
profile: {
name: "Анна",
address: {
city: "Москва"
}
}
}
};
// Без опциональной цепочки
const city1 = data.user.profile.address.city;
// С опциональной цепочкой (безопасно)
const city2 = data?.user?.profile?.address?.city;
// Что вернётся, если profile отсутствует?
const data2 = { user: {} };
const city3 = data2?.user?.profile?.address?.city; // undefined
Важно: ?. проверяет только ту часть, которая стоит перед ним. Часть после проверяется только если предыдущая существует.
javascript
// Разница между:
obj?.prop.method // prop проверяется, method — нет
obj?.prop?.method // проверяются и prop, и method
Часть 5. Сравнение с традиционными подходами
Подход 1: Проверка через if
javascript
let city;
if (user && user.address && user.address.city) {
city = user.address.city;
} else {
city = undefined;
}
Подход 2: Логическое И (&&)
javascript
const city = user && user.address && user.address.city;
Подход 3: try-catch (никто так не делает)
javascript
let city;
try {
city = user.address.city;
} catch {
city = undefined;
}
Подход 4: Опциональная цепочка (победитель)
javascript
const city = user?.address?.city;
Итог: Опциональная цепочка короче, чище и понятнее всех альтернатив.
Часть 6. Практические примеры
6.1 Работа с API
javascript
// До
const userName = response && response.data && response.data.user && response.data.user.name;
// После
const userName = response?.data?.user?.name;
6.2 Массивы
javascript
const users = [
{ name: "Анна", address: { city: "Москва" } },
{ name: "Борис" } // нет address
];
// Безопасный доступ к элементу массива и его свойству
const firstCity = users[0]?.address?.city; // "Москва"
const secondCity = users[1]?.address?.city; // undefined
const tenthCity = users[9]?.address?.city; // undefined
6.3 Вызов методов
javascript
const user = {
name: "Анна",
getAddress() {
return { city: "Москва" };
}
};
// Безопасный вызов метода
const city = user.getAddress?.()?.city; // "Москва"
// Если метода нет
const user2 = { name: "Борис" };
const city2 = user2.getAddress?.()?.city; // undefined
6.4 Динамические ключи
javascript
const key = "address";
const city = user?.[key]?.city;
6.5 Комбинация с nullish coalescing ??
javascript
const userName = user?.profile?.name ?? "Гость";
// Если user?.profile?.name === null/undefined → "Гость"
6.6 React-компоненты
jsx
function UserProfile({ user }) {
// Безопасно получаем имя
const name = user?.profile?.name ?? "Аноним";
return (
<div>
<h1>{name}</h1>
<p>Город: {user?.address?.city ?? "Не указан"}</p>
<img src={user?.avatar?.url ?? "/default-avatar.png"} />
</div>
);
}
Часть 7. Ограничения и подводные камни
Камень #1: Нельзя использовать для присваивания
javascript
// ❌ Ошибка! Нельзя присваивать через ?.
user?.name = "Борис";
// ✅ Правильно
if (user) {
user.name = "Борис";
}
Камень #2: Короткое замыкание
javascript
let user = null;
let x = 0;
user?.sayHi(x++); // user === null → x++ не выполняется!
console.log(x); // 0 (а не 1)
Камень #3: Не работает с примитивами (кроме null/undefined)
javascript
const num = 42;
console.log(num?.toString()); // "42" (работает!)
// Но осторожно:
console.log(null?.toString()); // undefined
console.log(undefined?.toString()); // undefined
Камень #4: Не спасает от ошибок в самом конце
javascript
const user = { name: "Анна" };
// ❌ Всё равно ошибка, если метода нет
user?.getAddress(); // TypeError: user.getAddress is not a function
// ✅ Правильно
user?.getAddress?.(); // undefined
Камень #5: Не работает с цепочкой вызовов функций
javascript
// ❌ Не сработает
const result = getUser()?.name;
// ✅ Нужно присвоить результат переменной
const user = getUser();
const name = user?.name;
Часть 8. Реальные кейсы из продакшена
8.1 Обработка ответа сервера
javascript
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
// Безопасный доступ к глубоким данным
const userName = data?.user?.personalInfo?.name ?? "Неизвестно";
const userEmail = data?.user?.contact?.email ?? "email@example.com";
const userRoles = data?.user?.permissions?.roles ?? [];
return { userName, userEmail, userRoles };
} catch (error) {
console.error("Ошибка загрузки:", error);
return null;
}
}
8.2 Работа с DOM
javascript
// Старый способ
const buttonText = document.querySelector('.btn')
&& document.querySelector('.btn').textContent;
// Новый способ
const buttonText = document.querySelector('.btn')?.textContent;
// С цепочкой методов
const parentId = document.querySelector('.modal')?.parentElement?.id;
8.3 Конфигурация приложения
javascript
const config = {
api: {
endpoints: {
users: "/api/users"
}
}
};
// Безопасное получение настроек
const apiUrl = config?.api?.baseUrl ?? "https://default.api.com";
const usersEndpoint = config?.api?.endpoints?.users ?? "/users";
const timeout = config?.api?.timeout ?? 5000;
8.4 Redux/Vuex стейт
javascript
// Без опциональной цепочки
const userName = state && state.user && state.user.profile && state.user.profile.name;
// С опциональной цепочкой
const userName = state?.user?.profile?.name;
// В редьюсере
function userReducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
return {
...state,
user: {
...state?.user,
profile: {
...state?.user?.profile,
name: action.payload.name
}
}
};
default:
return state;
}
}
Часть 9. Опциональная цепочка в разных средах
Браузеры
Node.js
bash
# Node.js 14+ поддерживает опциональную цепочку
node --version # 14.0.0 или выше
Транспиляция (Babel)
json
// .babelrc
{
"plugins": ["@babel/plugin-proposal-optional-chaining"]
}
Часть 10. Опциональная цепочка vs Другие подходы
javascript
const obj = { a: { b: { c: 42 } } };
// Lodash get (если используете библиотеку)
const value1 = _.get(obj, 'a.b.c');
// Опциональная цепочка (нативный)
const value2 = obj?.a?.b?.c;
// Ручная проверка
const value3 = obj && obj.a && obj.a.b && obj.a.b.c;
// try-catch (абсурд)
let value4;
try { value4 = obj.a.b.c; } catch { value4 = undefined; }
Плюсы опциональной цепочки:
- Нативная поддержка (без библиотек)
- Короткий синтаксис
- Работает с методами
- Высокая производительность
Минусы:
- Не работает для присваивания
- Требует современный браузер/транспиляцию
Часть 11. Комбинация с другими операторами
?. + ?? (идеальная пара)
javascript
// Значение по умолчанию, если что-то в цепочке отсутствует
const userName = user?.profile?.name ?? "Гость";
// Или для более сложной логики
const userAge = user?.profile?.age ?? (defaultAge > 0 ? defaultAge : 18);
?. + || (будьте осторожны)
javascript
// Плохо (0, "" заменятся)
const name = user?.profile?.name || "Гость";
// Хорошо (заменяет только null/undefined)
const name = user?.profile?.name ?? "Гость";
?. в шаблонных строках
javascript
const message = `Пользователь: ${user?.profile?.name ?? "Неизвестен"},
Город: ${user?.address?.city ?? "Не указан"}`;
Часть 12. Продвинутые техники
12.1 Своя реализация (для понимания)
javascript
function optionalChaining(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
const user = { profile: { name: "Анна" } };
console.log(optionalChaining(user, "profile.name")); // "Анна"
console.log(optionalChaining(user, "address.city")); // undefined
12.2 Условный рендеринг в React
jsx
function UserCard({ user }) {
return (
<div className="card">
<img src={user?.avatar?.url ?? "/default.png"} />
<h3>{user?.name ?? "Аноним"}</h3>
<p>{user?.bio?.short ?? "Нет описания"}</p>
{user?.isPremium && <Badge>Premium</Badge>}
<div className="stats">
<span>Посты: {user?.stats?.postsCount ?? 0}</span>
<span>Подписчики: {user?.stats?.followers ?? 0}</span>
</div>
</div>
);
}
Итог: Манифест опциональной цепочки
- Используйте ?. для безопасного доступа к глубоко вложенным свойствам.
- Не используйте ?. для присваивания - это не работает.
- Комбинируйте с ?? для значений по умолчанию.
- Помните про короткое замыкание - правая часть может не выполниться.
- Проверяйте методы перед вызовом - obj?.method?.().
- Не злоупотребляйте - если структура данных всегда определена, ?. не нужен.
Финальный тест (что выведет?):
javascript
const data = {
user: null
};
console.log(data?.user?.name);
console.log(data?.user?.getName?.());
console.log(data?.user?.address?.city ?? "Unknown");
let x = 0;
const result = data?.user?.getName(x++);
console.log(x);
Ответ:
- undefined (user = null)
- undefined (метода нет)
- "Unknown" (nullish coalescing)
- 0 (из-за короткого замыкания x++ не выполнился)
Опциональная цепочка - это не просто синтаксический сахар. Это инструмент, который делает код безопаснее и читаемее. Она позволяет забыть о бесконечных проверках на null и undefined и сосредоточиться на реальной логике.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Секретные ключи: Почему Symbol - самый недооценённый тип данных в JavaScript
Вы знаете string, number, boolean, object, undefined, null, bigint. Но есть ещё один тип. Таинственный. Скрытый. Многие о нём слышали, но почти никто не использует.
Встречайте Symbol - примитивный тип данных, который появился в ES6 и изменил правила игры. Символы уникальны, невидимы в циклах и идеальны для создания приватных свойств. Они - секретные ключи от ваших объектов.
Сегодня мы раскроем все тайны символов и покажем, где они действительно незаменимы.
Часть 1. Что такое Symbol?
Symbol - это уникальный и неизменяемый примитив, который часто используют как идентификатор для свойств объектов.
javascript
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false (каждый символ уникален)
console.log(typeof sym1); // "symbol"
Главное свойство: каждый символ уникален. Даже если создать два символа с одинаковым описанием, они будут разными.
javascript
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false (разные символы!)
Часть 2. Зачем нужны символы? (Мотивация)
Представьте, что вы пишете библиотеку, которая добавляет методы к объектам. Как избежать конфликта имён с другими библиотеками?
javascript
// Ваша библиотека
function myLibrary(obj) {
obj.id = "myLibrary-123"; // Ой! А если у объекта уже есть свойство id?
}
// Другая библиотека
function otherLibrary(obj) {
obj.id = "otherLibrary-456"; // Конфликт!
}
Решение: символы.
javascript
const MY_ID = Symbol("myLibraryId");
function myLibrary(obj) {
obj[MY_ID] = "myLibrary-123"; // Никто не создаст такой же символ
}
// Даже если другой символ назовётся так же, он будет другим
const ANOTHER_ID = Symbol("myLibraryId"); // другой символ!
Часть 3. Создание символов
3.1 Базовый символ
javascript
const sym = Symbol();
console.log(sym); // Symbol()
3.2 Символ с описанием (для отладки)
javascript
const sym = Symbol("user.id");
console.log(sym); // Symbol(user.id)
console.log(sym.description); // "user.id" (ES2019)
3.3 Глобальные символы (реестр)
javascript
// Создание или получение из глобального реестра
const globalSym = Symbol.for("app.theme");
const sameGlobalSym = Symbol.for("app.theme");
console.log(globalSym === sameGlobalSym); // true (один и тот же символ)
// Получить ключ из реестра
console.log(Symbol.keyFor(globalSym)); // "app.theme"
Разница:
- Symbol("key") - уникальный локальный символ
- Symbol.for("key") - глобальный символ (один на всю программу)
Часть 4. Символы как ключи объектов
Символы могут быть ключами объектов (наравне со строками).
javascript
const id = Symbol("id");
const user = {
name: "Анна",
[id]: 12345 // символ как ключ
};
console.log(user[id]); // 12345
console.log(user.id); // undefined (обычное свойство)
Особенность: свойства-символы не появляются в обычных итерациях.
javascript
const sym = Symbol("secret");
const obj = {
name: "Анна",
age: 25,
[sym]: "скрытое значение"
};
console.log(Object.keys(obj)); // ["name", "age"] (символ не попал)
console.log(Object.values(obj)); // ["Анна", 25]
console.log(JSON.stringify(obj)); // {"name":"Анна","age":25}
// Но символы доступны через специальные методы
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(secret)]
console.log(Reflect.ownKeys(obj)); // ["name", "age", Symbol(secret)]
Часть 5. Где символы незаменимы
5.1 Скрытые метаданные
javascript
const user = { name: "Анна" };
const createdAt = Symbol("createdAt");
const updatedAt = Symbol("updatedAt");
user[createdAt] = Date.now();
user[updatedAt] = Date.now();
// Обычные методы не видят символы
console.log(JSON.stringify(user)); // {"name":"Анна"}
// Но мы можем к ним обратиться
console.log(user[createdAt]); // 1672531200000
5.2 Избегание конфликтов в библиотеках
javascript
// Библиотека 1
const _internal = Symbol("internal");
class Library1 {
constructor() {
this[_internal] = { version: "1.0.0" };
}
}
// Библиотека 2 (использует тот же класс)
const _internal2 = Symbol("internal"); // другой символ!
class Library2 {
enhance(obj) {
obj[_internal2] = { enhanced: true };
}
}
// Конфликта нет!
5.3 Встроенные символы (Symbol.*)
JavaScript предоставляет встроенные символы для настройки поведения объектов.
Symbol.iterator - делаем объект итерируемым
javascript
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
Symbol.toStringTag - кастомное строковое представление
javascript
class MyClass {
get [Symbol.toStringTag]() {
return "MyCustomClass";
}
}
const obj = new MyClass();
console.log(Object.prototype.toString.call(obj)); // "[object MyCustomClass]"
Symbol.toPrimitive - контроль преобразования типов
javascript
const obj = {
value: 42,
[Symbol.toPrimitive](hint) {
if (hint === "string") {
return `Значение: ${this.value}`;
}
if (hint === "number") {
return this.value;
}
return null;
}
};
console.log(String(obj)); // "Значение: 42"
console.log(+obj); // 42
console.log(obj + ""); // "null"
Symbol.hasInstance - кастомный instanceof
javascript
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true (хотя [] не создан через MyArray!)
Часть 6. Полный список встроенных символов
Часть 7. Реальные паттерны использования
7.1 Приватные поля (до реальных приватных полей #)
javascript
const _balance = Symbol("balance");
const _validate = Symbol("validate");
class BankAccount {
constructor(initialBalance) {
this[_balance] = initialBalance;
}
[_validate](amount) {
if (amount < 0) throw new Error("Сумма не может быть отрицательной");
if (amount > this[_balance]) throw new Error("Недостаточно средств");
}
withdraw(amount) {
this[_validate](amount);
this[_balance] -= amount;
return this[_balance];
}
getBalance() {
return this[_balance];
}
}
const account = new BankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account._balance); // undefined (не видно)
console.log(account[Symbol("balance")]); // undefined (другой символ)
7.2 Константы enum (без конфликтов)
javascript
const Colors = {
RED: Symbol("red"),
GREEN: Symbol("green"),
BLUE: Symbol("blue")
};
function getColorName(color) {
switch (color) {
case Colors.RED: return "Красный";
case Colors.GREEN: return "Зелёный";
case Colors.BLUE: return "Синий";
default: return "Неизвестный";
}
}
console.log(getColorName(Colors.RED)); // "Красный"
// Никто случайно не сравнит Colors.RED со строкой "red" (разные типы)
7.3 Метапрограммирование (перехват операций)
javascript
const handler = {
get(target, prop, receiver) {
if (prop === Symbol.toPrimitive) {
return () => "Перехвачено!";
}
return Reflect.get(target, prop, receiver);
}
};
const obj = new Proxy({}, handler);
console.log(String(obj)); // "Перехвачено!"
7.4 Фреймворки и DI контейнеры
javascript
const INJECTION_KEY = Symbol("di:injection");
class Container {
constructor() {
this[INJECTION_KEY] = new Map();
}
register(token, factory) {
this[INJECTION_KEY].set(token, factory);
}
resolve(token) {
const factory = this[INJECTION_KEY].get(token);
if (!factory) throw new Error(`No registration for ${String(token)}`);
return factory();
}
}
const USER_SERVICE = Symbol("UserService");
const container = new Container();
container.register(USER_SERVICE, () => ({ getUsers: () => ["Анна", "Борис"] }));
const userService = container.resolve(USER_SERVICE);
console.log(userService.getUsers()); // ["Анна", "Борис"]
Часть 8. Подводные камни
Камень #1: Символы не преобразуются в строки автоматически
javascript
const sym = Symbol("id");
console.log("Symbol: " + sym); // TypeError: Cannot convert a Symbol value to a string
// Правильно
console.log(`Symbol: ${String(sym)}`); // "Symbol: Symbol(id)"
console.log(`Symbol: ${sym.description}`); // "Symbol: id"
Камень #2: Символы не сериализуются в JSON
javascript
const obj = {
name: "Анна",
[Symbol("secret")]: "секрет"
};
console.log(JSON.stringify(obj)); // {"name":"Анна"} (символ пропал)
Камень #3: Object.assign копирует символы (но осторожно)
javascript
const sym = Symbol("test");
const obj1 = { [sym]: 42 };
const obj2 = Object.assign({}, obj1);
console.log(obj2[sym]); // 42 (скопировался)
// Но в отличие от обычных свойств, символы не перечисляются
console.log(Object.keys(obj2)); // []
Камень #4: Глобальные символы не собираются мусором
javascript
// Symbol.for создаёт символ в глобальном реестре
// Он живёт, пока живёт реестр (всё приложение)
Symbol.for("never-garbage-collected");
Часть 9. Symbol vs другие подходы
Symbol vs String как ключи
Symbol vs WeakMap (для приватных данных)
javascript
// Symbol (простой, но обходим)
const _secret = Symbol("secret");
class User {
constructor(name) {
this[_secret] = name;
}
}
// WeakMap (более защищённый)
const secrets = new WeakMap();
class User2 {
constructor(name) {
secrets.set(this, name);
}
getName() {
return secrets.get(this);
}
}
Часть 10. Производительность
Символы очень быстрые. Их использование практически не влияет на производительность.
javascript
// Тест (условный)
console.time("string-key");
for (let i = 0; i < 1000000; i++) {
const obj = {};
obj["key"] = i;
const val = obj["key"];
}
console.timeEnd("string-key"); // ~15ms
console.time("symbol-key");
const sym = Symbol("key");
for (let i = 0; i < 1000000; i++) {
const obj = {};
obj[sym] = i;
const val = obj[sym];
}
console.timeEnd("symbol-key"); // ~15ms (такая же скорость)
Часть 11. Когда использовать Symbol?
✅ Хорошие кандидаты:
- Уникальные идентификаторы для свойств (особенно в библиотеках)
- Мета-свойства, которые не должны мешать обычной итерации
- Внутренние флаги и состояния в классах
- Константы enum (избегание конфликтов)
- Кастомизация поведения объектов (через встроенные символы)
❌ Плохие кандидаты:
- Данные, которые нужно сериализовать в JSON
- Свойства, которые должны быть видны в Object.keys
- Простые случаи, когда строка подойдёт (не усложняйте)
Итог: Манифест символов
- Symbol - уникальный примитив - два символа никогда не равны.
- Символы не видны в обычных итерациях - идеальны для метаданных.
- Symbol.for() создаёт глобальные символы (один на всю программу).
- Встроенные символы (Symbol.iterator, Symbol.toPrimitive) управляют поведением объектов.
- Не сериализуются в JSON - помните об этом.
- Идеальны для констант enum - избегают конфликтов.
- Не для всего - если нужна сериализация, используйте строки.
Финальный тест (что выведет?):
javascript
const sym1 = Symbol("id");
const sym2 = Symbol("id");
const sym3 = Symbol.for("id");
const sym4 = Symbol.for("id");
console.log(sym1 === sym2);
console.log(sym3 === sym4);
console.log(sym1 === sym3);
const obj = {
[sym1]: "value1",
id: "value2"
};
console.log(Object.keys(obj));
console.log(Object.getOwnPropertySymbols(obj));
console.log(JSON.stringify(obj));
Ответ:
- false (разные локальные символы)
- true (один глобальный символ)
- false (локальный vs глобальный)
- ["id"] (только строковый ключ)
- [Symbol(id), Symbol(id)] (два символа)
- {"id":"value2"} (символы не попали)
Symbol - это тайное оружие JavaScript. Оно редко нужно, но когда нужно — незаменимо. Если вы пишете библиотеку, фреймворк или работаете с метапрограммированием, символы станут вашими лучшими друзьями. В обычном прикладном коде они встречаются реже, но знание о них делает вас более глубоким разработчиком. Используйте символы с умом, и ваш код станет надёжнее и чище.
--------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------
Алхимия типов: Как JavaScript превращает объекты в примитивы (и почему это важно)
Вы когда-нибудь складывали два объекта? Или сравнивали их с числом? Или выводили в консоль и получали [object Object]?
JavaScript - мастер превращений. Когда он встречает объект там, где ожидается число, строка или булево значение, он запускает сложный механизм алхимического преобразования. Этот механизм скрыт от глаз, но управляет поведением вашего кода каждый день.
Сегодня мы заглянем за кулисы и узнаем, как объекты становятся примитивами, как управлять этим процессом и почему [] + [] возвращает пустую строку.
Часть 1. Зачем вообще преобразовывать объекты?
JavaScript - язык с динамической типизацией. Он постоянно приводит значения к нужным типам:
javascript
const obj = { value: 42 };
console.log(obj + 10); // "[object Object]10" (объект стал строкой)
console.log(obj - 10); // NaN (попытка стать числом)
console.log(obj == true); // false (объект сравнивается с булевым)
В каждой из этих операций JavaScript запускает скрытый механизм преобразования объекта в примитив. Понимание этого механизма помогает писать предсказуемый код и создавать объекты с кастомным поведением.
Часть 2. Три хинта: string, number, default
У преобразования объектов есть три "подсказки" (hints), которые говорят JavaScript, какой тип ожидается:
2.1 "string" - ожидается строка
javascript
// Операции, которые вызывают string-преобразование:
alert(obj); // alert ожидает строку
String(obj); // явное преобразование в строку
obj + "hello"; // плюс со строкой
obj.property; // не вызывает преобразование!
2.2 "number" - ожидается число
javascript
// Операции, вызывающие number-преобразование:
Number(obj); // явное преобразование
+obj; // унарный плюс
obj * 5; // математические операции
obj > 10; // сравнения (кроме ===)
Math.sqrt(obj); // математические функции
2.3 "default" - неясно, что ожидается
javascript
// Ситуации, когда hint = "default":
obj == "hello"; // нестрогое сравнение со строкой
obj + 10; // плюс (может быть сложением или конкатенацией)
obj == 5; // нестрогое сравнение с числом
Важно: Для большинства встроенных объектов "default" обрабатывается так же, как "number" (кроме Date и некоторых других).
Часть 3. Алгоритм преобразования (ToPrimitive)
Когда JavaScript встречает объект там, где нужен примитив, он запускает алгоритм ToPrimitive(input, hint):
javascript
// Псевдокод алгоритма
function ToPrimitive(obj, hint) {
// 1. Если obj уже примитив → вернуть obj
// 2. Если есть метод obj[Symbol.toPrimitive] → вызвать его
if (obj[Symbol.toPrimitive]) {
const result = obj[Symbol.toPrimitive](hint);
if (isPrimitive(result)) return result;
throw new TypeError();
}
// 3. Если hint === "string"
if (hint === "string") {
const result = obj.toString();
if (isPrimitive(result)) return result;
const result2 = obj.valueOf();
if (isPrimitive(result2)) return result2;
throw new TypeError();
}
// 4. Если hint === "number" или "default"
if (hint === "number" || hint === "default") {
const result = obj.valueOf();
if (isPrimitive(result)) return result;
const result2 = obj.toString();
if (isPrimitive(result2)) return result2;
throw new TypeError();
}
}
Часть 4. Методы toString() и valueOf()
Каждый объект наследует методы toString() и valueOf() от Object.prototype.
4.1 toString() - строковое представление
javascript
const obj = { a: 1 };
console.log(obj.toString()); // "[object Object]"
const arr = [1, 2, 3];
console.log(arr.toString()); // "1,2,3"
const date = new Date();
console.log(date.toString()); // "Sun Apr 12 2026 15:30:00 GMT+0700"
4.2 valueOf() - числовое представление
javascript
const obj = { a: 1 };
console.log(obj.valueOf()); // { a: 1 } (возвращает сам объект)
const arr = [1, 2, 3];
console.log(arr.valueOf()); // [1, 2, 3] (возвращает сам объект)
const date = new Date();
console.log(date.valueOf()); // 1672531200000 (timestamp)
Важно: Для большинства объектов valueOf() возвращает сам объект (не примитив). Поэтому в преобразовании обычно участвует toString().
Часть 5. Встроенные объекты и их преобразование
5.1 Массивы
javascript
const arr = [1, 2, 3];
console.log(String(arr)); // "1,2,3" (toString)
console.log(Number(arr)); // NaN (valueOf → [1,2,3] → toString → "1,2,3" → NaN)
console.log(arr + 5); // "1,2,35" (строковое преобразование)
console.log(arr - 5); // NaN (числовое преобразование)
5.2 Пустой массив
javascript
const empty = [];
console.log(String(empty)); // "" (пустая строка)
console.log(Number(empty)); // 0 ("" → 0)
console.log(empty + 5); // "5" ("" + 5 = "5")
console.log(empty - 5); // -5 (0 - 5 = -5)
5.3 Дата (особый случай)
Date - единственный встроенный объект, у которого "default" обрабатывается как "string".
javascript
const date = new Date();
console.log(date + 5); // "Sun Apr 12 2026...5" (строковое преобразование)
console.log(date - 5); // число (timestamp - 5)
console.log(+date); // timestamp (числовое преобразование)
5.4 Функции
javascript
function greet() { return "Hello"; }
console.log(String(greet)); // "function greet() { return "Hello"; }"
console.log(Number(greet)); // NaN
console.log(greet + 5); // "function greet...5"
Часть 6. Symbol.toPrimitive - контроль над преобразованием
Современный способ управлять преобразованием - метод [Symbol.toPrimitive].
javascript
const customObj = {
value: 42,
[Symbol.toPrimitive](hint) {
console.log(`Хинт: ${hint}`);
switch (hint) {
case "string":
return `Значение: ${this.value}`;
case "number":
return this.value;
case "default":
return this.value * 2;
}
}
};
console.log(String(customObj)); // "Значение: 42" (hint: string)
console.log(+customObj); // 42 (hint: number)
console.log(customObj + 5); // 89 (hint: default, 42*2+5=89)
Часть 7. Сравнение объектов
7.1 Строгое сравнение (===)
При строгом сравнении преобразования НЕТ. Сравниваются ссылки.
javascript
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;
console.log(obj1 === obj2); // false (разные объекты)
console.log(obj1 === obj3); // true (одна ссылка)
7.2 Нестрогое сравнение (==)
При нестрогом сравнении объекты преобразуются в примитивы.
javascript
const obj = { valueOf: () => 42 };
console.log(obj == 42); // true (obj → 42)
console.log(obj == "42"); // true (obj → 42 → "42")
7.3 Сравнение массивов
javascript
console.log([] == false); // true ([] → "" → 0, false → 0)
console.log([] == 0); // true ([] → "" → 0)
console.log([1] == 1); // true ([1] → "1" → 1)
console.log([1,2] == 3); // false ([1,2] → "1,2" → NaN)
Часть 8. Классические загадки (разбор)
8.1 [] + []
javascript
console.log([] + []); // ""
// 1. Hint = "default" (оператор +)
// 2. [][Symbol.toPrimitive]? нет
// 3. valueOf() → [] (не примитив)
// 4. toString() → "" (примитив)
// 5. "" + "" = ""
8.2 [] + {}
javascript
console.log([] + {}); // "[object Object]"
// [] → "" (как выше)
// {} → valueOf() → {} (не примитив)
// {} → toString() → "[object Object]"
// "" + "[object Object]" = "[object Object]"
8.3 {} + []
javascript
console.log({} + []); // 0 (в некоторых средах)
// ВАЖНО: в начале строки {} интерпретируется как блок кода!
// Поэтому реально выполняется: + []
// + [] → Number([]) → Number("") → 0
8.4 ![]
javascript
console.log(![]); // false
// [] → true (объект всегда true)
// !true → false
8.5 [] == ![]
javascript
console.log([] == ![]); // true
// Шаг 1: ![] → false
// Шаг 2: [] == false
// Шаг 3: ToNumber([]) → ToNumber("") → 0
// Шаг 4: ToNumber(false) → 0
// Шаг 5: 0 == 0 → true
Часть 9. Создание объектов с кастомным преобразованием
9.1 Числоподобный объект
javascript
const NumberLike = {
value: 10,
[Symbol.toPrimitive](hint) {
if (hint === "number" || hint === "default") {
return this.value;
}
return String(this.value);
},
increment() {
this.value++;
return this;
}
};
console.log(NumberLike + 5); // 15
console.log(NumberLike * 2); // 20
console.log(String(NumberLike)); // "10"
NumberLike.increment();
console.log(NumberLike + 5); // 16
9.2 Диапазон чисел
javascript
const Range = {
from: 1,
to: 10,
[Symbol.toPrimitive](hint) {
if (hint === "string") {
return `[${this.from}..${this.to}]`;
}
if (hint === "number") {
return (this.from + this.to) / 2; // среднее значение
}
return this.to - this.from; // длина диапазона
}
};
console.log(String(Range)); // "[1..10]"
console.log(+Range); // 5.5
console.log(Range + 0); // 9 (длина 9)
9.3 Безопасные вычисления
javascript
class SafeNumber {
constructor(value) {
this.value = value;
}
[Symbol.toPrimitive](hint) {
if (hint === "number") {
if (isNaN(this.value)) return 0;
if (!isFinite(this.value)) return 0;
return this.value;
}
if (hint === "string") {
return String(this.value);
}
return this.value;
}
}
const safe = new SafeNumber(NaN);
console.log(safe + 10); // 10 (NaN превратился в 0)
Часть 10. Преобразование в булевы значения
Преобразование объекта в булево значение происходит по простому правилу: любой объект → true (даже пустой).
javascript
console.log(Boolean({})); // true
console.log(Boolean([])); // true
console.log(Boolean(new Boolean(false))); // true (объект-обёртка)
Исключений нет. Это важно помнить:
javascript
if ([]) {
console.log("Пустой массив - true!");
} // Выполнится!
if ({}) {
console.log("Пустой объект - true!");
} // Выполнится!
Часть 11. Практические применения
11.1 Автоматическая валидация
javascript
class ValidatedNumber {
constructor(value, min, max) {
this.value = value;
this.min = min;
this.max = max;
}
[Symbol.toPrimitive](hint) {
if (hint === "number") {
if (this.value < this.min) return this.min;
if (this.value > this.max) return this.max;
return this.value;
}
return String(this.value);
}
}
const age = new ValidatedNumber(150, 0, 120);
console.log(age + 5); // 125 (ограничено 120)
11.2 Ленивые вычисления
javascript
class LazyValue {
constructor(computeFn) {
this.computeFn = computeFn;
this.cached = null;
this.computed = false;
}
[Symbol.toPrimitive](hint) {
if (!this.computed) {
this.cached = this.computeFn();
this.computed = true;
}
if (hint === "string") {
return String(this.cached);
}
if (hint === "number") {
return Number(this.cached);
}
return this.cached;
}
}
const expensive = new LazyValue(() => {
console.log("Тяжёлые вычисления...");
return 42;
});
console.log(expensive + 5); // "Тяжёлые вычисления..." → 47
console.log(expensive * 2); // 84 (используется кэш, повторных вычислений нет)
11.3 Единицы измерения
javascript
class Distance {
constructor(meters) {
this.meters = meters;
}
static km(value) {
return new Distance(value * 1000);
}
[Symbol.toPrimitive](hint) {
if (hint === "number") {
return this.meters;
}
if (hint === "string") {
if (this.meters >= 1000) {
return `${this.meters / 1000} км`;
}
return `${this.meters} м`;
}
return this.meters;
}
}
const dist = Distance.km(5);
console.log(dist + 500); // 5500 (метры)
console.log(String(dist)); // "5 км"
console.log(dist < 6000); // true
Часть 12. Подводные камни
Камень #1: Неявное преобразование в логических операторах
javascript
const obj = {
valueOf: () => 0,
toString: () => "true"
};
if (obj) {
console.log("Это выполнится всегда!"); // Объект всегда true
}
if (obj == true) {
console.log("А это зависит от преобразования");
}
Камень #2: Преобразование Date
javascript
const date = new Date(2024, 0, 1);
console.log(date + 1); // "Mon Jan 01 2024 ...1" (строка)
console.log(date - 1); // число (timestamp - 1)
// Из-за разного поведения сложения и вычитания
Камень #3: Symbol.toPrimitive должен возвращать примитив
javascript
const obj = {
[Symbol.toPrimitive]() {
return {}; // ❌ возвращает объект
}
};
console.log(obj + 5); // TypeError: Cannot convert object to primitive value
Камень #4: valueOf и toString могут вернуть примитив, но не обязаны
javascript
const obj = {
valueOf() {
return {}; // ❌ вернули объект
},
toString() {
return {}; // ❌ вернули объект
}
};
console.log(obj + 5); // TypeError: Cannot convert object to primitive value
Итог: Манифест преобразования
- Три хинта: "string", "number", "default".
- Алгоритм: Symbol.toPrimitive → valueOf → toString.
- Объекты всегда true в булевом контексте.
- Date — особый случай: "default" = "string".
- Symbol.toPrimitive — современный способ контроля преобразования.
- Пустой массив → "" → 0.
- valueOf для обычных объектов возвращает сам объект (не примитив).
Финальный тест (что выведет?):
javascript
const obj = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 10;
if (hint === "string") return "twenty";
return 30;
}
};
console.log(+obj);
console.log(String(obj));
console.log(obj + 5);
console.log(obj == 30);
Ответ:
- 10 (hint: number)
- "twenty" (hint: string)
- 35 (hint: default → 30 + 5)
- true (30 == 30)
Преобразование объектов в примитивы - одна из самых сложных и непонятых тем в JavaScript. Но когда вы понимаете механизм, перестаёте удивляться [] + [] === "" и начинаете использовать эту силу для создания элегантных API. Управляйте преобразованиями своих объектов, и ваш код станет и красивее, и предсказуемее.