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

Язык JavaScript - Объекты: основы

Вы думаете, что знаете объекты? { key: value } - что тут сложного? Но JavaScript-объекты скрывают тайны, о которых вы не догадывались. Они могут создаваться без прототипа, иметь вычисляемые ключи, скрытые свойства и даже перехватывать любое обращение к себе. Объекты в JavaScript - это не просто "словари" из других языков. Это динамические, гибкие, живые структуры, которые могут меняться прямо во время выполнения. И если массивы - это просто разновидность объектов, а функции - это объекты с возможностью вызова, то понимание объектов открывает дверь к пониманию всего языка. В JavaScript объект - это коллекция свойств. Свойство - это связь между ключом (строкой или символом) и значением (любым типом). javascript const user = {
name: "Анна", // свойство с ключом "name"
age: 25, // свойство с ключом "age"
"has cat": true // ключ с пробелом (требует кавычек)
};
// Доступ через точку
console.log(user.name); // "Анна"
// Доступ через квадратные скобки (для любых ключе
Оглавление
картинка взяты с ya.ru
картинка взяты с ya.ru

Всё есть объект (почти): Путеводитель по вселенной 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 методы, которые нужно знать

-2

Часть 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 или массивы пар

Итог: Манифест объектов

  1. Объекты - по ссылке - копирование создаёт новый объект, присваивание - ссылку.
  2. Ключи - строки или символы - числа превращаются в строки.
  3. Дескрипторы - контролируйте доступ через defineProperty.
  4. Прототипы - понимайте цепочку, используйте Object.create для чистых словарей.
  5. Итерация - Object.keys для своих свойств, for...in + hasOwnProperty для своих+наследуемых.
  6. Копирование - structuredClone для глубокого, {...obj} для поверхностного.
  7. Геттеры/сеттеры - для вычисляемых свойств и валидации.

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

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. Сравнение методов копирования

-3

Часть 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. Чек-лист: Какую копию выбрать?

-4

Итог: Манифест копирования

  1. Примитивы копируются по значению - безопасны.
  2. Объекты копируются по ссылке - мутация через любую ссылку меняет оригинал.
  3. Поверхностное копирование - {...obj} - копирует только первый уровень.
  4. Глубокое копирование - structuredClone - полная независимость.
  5. JSON метод - быстр, но теряет функции, Date, undefined, Symbol.
  6. Всегда думайте о вложенности - самый частый источник багов.
  7. Иммутабельность - ключ к предсказуемому коду (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

  1. Heap snapshot - снимок памяти. Сделайте два снимка и сравните.
  2. Allocation timeline - запись выделений в реальном времени.
  3. 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>;
}

Итог: Манифест уборщика

  1. Сборка мусора автоматическая - не вызывайте её вручную (даже если очень хочется).
  2. Mark-and-Sweep - основа современных алгоритмов (работает с циклами).
  3. Поколенческая сборка - новые объекты собираются чаще, старые — реже.
  4. Утечки памяти реальны - глобальные переменные, забытые таймеры, слушатели, замыкания.
  5. WeakMap/WeakSet - ваши друзья для кэширования без утечек.
  6. DevTools Memory Tab - лучший способ найти утечку.
  7. Чистите за собой - обнуляйте ссылки, удаляйте слушатели, отменяйте таймеры.

Финальный тест: что произойдёт с памятью?

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

-5

Часть 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 (шпаргалка)

-6

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

  1. Значение this определяется в момент вызова, а не в момент создания (кроме стрелок).
  2. Стрелочные функции - самый простой способ сохранить this (но не для методов объектов).
  3. .bind() - создаёт функцию с навсегда привязанным this.
  4. .call() / .apply() - вызывают функцию с заданным this (один раз).
  5. В методах объектов используйте обычные функции (не стрелки), если хотите иметь доступ к объекту через this.
  6. В колбэках всегда проверяйте, не потерялся ли this (используйте стрелку, bind или сохраните в переменную).
  7. Классы - метод, переданный как колбэк, теряет 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 делает четыре вещи:

  1. 🏗️ Создаёт новый пустой объект
  2. 🔗 Связывает его с прототипом конструктора
  3. 🎯 Вызывает конструктор с this, указывающим на новый объект
  4. 🎁 Возвращает новый объект (или то, что вернул конструктор, если это объект)

Часть 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% случаев не важна

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

  1. new создаёт объект, связывает прототип, вызывает конструктор, возвращает объект.
  2. Забытый new - классическая ошибка. Используйте защиту или фабрики.
  3. Методы лучше добавлять в prototype - экономия памяти.
  4. instanceof проверяет принадлежность к конструктору (по прототипу).
  5. Стрелочные функции не могут быть конструкторами.
  6. Классы ES6 - современный синтаксис для конструкторов.
  7. Фабричные функции - проще и безопаснее, но чуть медленнее.

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

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. Опциональная цепочка в разных средах

Браузеры

-7

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>
);
}

Итог: Манифест опциональной цепочки

  1. Используйте ?. для безопасного доступа к глубоко вложенным свойствам.
  2. Не используйте ?. для присваивания - это не работает.
  3. Комбинируйте с ?? для значений по умолчанию.
  4. Помните про короткое замыкание - правая часть может не выполниться.
  5. Проверяйте методы перед вызовом - obj?.method?.().
  6. Не злоупотребляйте - если структура данных всегда определена, ?. не нужен.

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

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. Полный список встроенных символов

-8

Часть 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 как ключи

-9

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
  • Простые случаи, когда строка подойдёт (не усложняйте)

Итог: Манифест символов

  1. Symbol - уникальный примитив - два символа никогда не равны.
  2. Символы не видны в обычных итерациях - идеальны для метаданных.
  3. Symbol.for() создаёт глобальные символы (один на всю программу).
  4. Встроенные символы (Symbol.iterator, Symbol.toPrimitive) управляют поведением объектов.
  5. Не сериализуются в JSON - помните об этом.
  6. Идеальны для констант enum - избегают конфликтов.
  7. Не для всего - если нужна сериализация, используйте строки.

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

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

Итог: Манифест преобразования

  1. Три хинта: "string", "number", "default".
  2. Алгоритм: Symbol.toPrimitive → valueOf → toString.
  3. Объекты всегда true в булевом контексте.
  4. Date — особый случай: "default" = "string".
  5. Symbol.toPrimitive — современный способ контроля преобразования.
  6. Пустой массив → "" → 0.
  7. 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. Управляйте преобразованиями своих объектов, и ваш код станет и красивее, и предсказуемее.