Пошук уроків, статей та іншого контенту
Розберіть фундаментальну різницю між примітивними значеннями та об'єктами. Дізнайтеся, як ця різниця впливає на присвоєння, порівняння та зміну даних.
У JavaScript усі значення можна умовно поділити на дві великі групи:
примітивні значення;
об'єкти — їх часто називають reference types, або типами за посиланням.
До примітивних типів належать:
string;
number;
bigint;
boolean;
undefined;
symbol;
null.
Примітивне значення є неподільним: воно не містить властивостей або вкладених значень, якими можна керувати окремо.
const name = "Olena";
const age = 28;
const isAdmin = false;
console.log(typeof name); // string
console.log(typeof age); // number
console.log(typeof isAdmin); // booleanОсобливий випадок:
console.log(typeof null); // objectЦе історична особливість JavaScript. Незважаючи на результат typeof, null є примітивним значенням, а не об'єктом.
Об'єкти можуть містити властивості та вкладені значення. До об'єктів належать:
звичайні об'єкти;
масиви;
функції;
дати;
регулярні вирази;
екземпляри класів;
інші спеціальні об'єкти.
const user = {
name: "Olena",
age: 28
};
const numbers = [10, 20, 30];
function greet() {
return "Привіт!";
}
console.log(typeof user); // object
console.log(typeof numbers); // object
console.log(typeof greet); // functionМасиви та функції також є об'єктами, хоча typeof повертає для функції окремий результат — "function".
Перевірити, чи є значення масивом, можна за допомогою Array.isArray():
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray({})); // falseКоли примітивне значення присвоюється іншій змінній, нова змінна отримує незалежну копію значення.
let firstName = "Olena";
let secondName = firstName;
secondName = "Andrii";
console.log(firstName); // Olena
console.log(secondName); // AndriiЗміна secondName не вплинула на firstName, оскільки змінні містять окремі примітивні значення.
Це працює для всіх примітивних типів:
let firstNumber = 10;
let secondNumber = firstNumber;
secondNumber += 5;
console.log(firstNumber); // 10
console.log(secondNumber); // 15Під час присвоєння об'єкта змінна отримує не незалежну копію всіх властивостей, а значення, яке посилається на той самий об'єкт.
const firstUser = {
name: "Olena"
};
const secondUser = firstUser;
secondUser.name = "Andrii";
console.log(firstUser.name); // Andrii
console.log(secondUser.name); // AndriifirstUser і secondUser посилаються на один об'єкт у пам'яті. Тому зміна об'єкта через одну змінну помітна через іншу.
Схематично це можна уявити так:
firstUser ──┐
├──> { name: "Andrii" }
secondUser ──┘Змінні не містять дві різні копії об'єкта.
Примітивні значення порівнюються за своїм значенням.
console.log(10 === 10); // true
console.log("hello" === "hello"); // true
console.log(true === true); // trueНавіть якщо однакові рядки були створені в різних місцях програми, вони мають однакове значення:
const firstText = "JavaScript";
const secondText = "JavaScript";
console.log(firstText === secondText); // trueДля порівняння переважно слід використовувати строге порівняння ===. Воно не виконує неочікуване автоматичне перетворення типів.
console.log(5 === "5"); // false
console.log(5 == "5"); // trueОб'єкти порівнюються за посиланням, а не за вмістом.
const firstObject = { value: 1 };
const secondObject = { value: 1 };
console.log(firstObject === secondObject); // falseХоча об'єкти мають однакові властивості, це два різні об'єкти.
firstObject ──> { value: 1 }
secondObject ──> { value: 1 }Навіть два порожні об'єкти не вважаються рівними:
console.log({} === {}); // falseОднак дві змінні, які посилаються на один об'єкт, є рівними:
const firstObject = { value: 1 };
const secondObject = firstObject;
console.log(firstObject === secondObject); // trueТут порівнюється не вміст, а те, чи посилаються змінні на один і той самий об'єкт.
Важливо розрізняти:
зміну властивості об'єкта;
переприсвоєння змінної.
let user = {
name: "Olena"
};
user.name = "Andrii"; // зміна об'єкта
user = { name: "Marta" }; // нове посиланняУ першому рядку змінюється властивість об'єкта, на який посилається user.
У другому рядку змінна user починає посилатися на інший об'єкт.
const з об'єктамиconst забороняє переприсвоювати змінну, але не робить об'єкт незмінним.
const user = {
name: "Olena"
};
user.name = "Andrii"; // дозволено
// user = { name: "Marta" }; // TypeErrorЗмінити властивість можна, оскільки об'єкт залишається тим самим. Заборонено лише надати змінній нове значення.
Те саме стосується масивів:
const numbers = [1, 2, 3];
numbers.push(4); // дозволено
console.log(numbers); // [1, 2, 3, 4]Якщо потрібно заборонити зміну властивостей на верхньому рівні об'єкта, можна використати Object.freeze():
const user = Object.freeze({
name: "Olena"
});
// У нестрогому режимі зміна може бути проігнорована.
// У строгому режимі така операція спричинить помилку.
user.name = "Andrii";
console.log(user.name); // OlenaObject.freeze() є поверхневим: вкладені об'єкти безпосередньо не заморожуються.
Щоб створити окремий об'єкт, можна виконати поверхневе копіювання за допомогою spread-синтаксису:
const originalUser = {
name: "Olena",
age: 28
};
const copiedUser = {
...originalUser
};
copiedUser.name = "Andrii";
console.log(originalUser.name); // Olena
console.log(copiedUser.name); // AndriiТепер змінні посилаються на різні об'єкти:
console.log(originalUser === copiedUser); // falseТакож можна використати Object.assign():
const original = {
enabled: true,
count: 3
};
const copy = Object.assign({}, original);
copy.count = 10;
console.log(original.count); // 3
console.log(copy.count); // 10Spread-синтаксис копіює лише властивості верхнього рівня. Якщо властивість містить інший об'єкт або масив, вкладене значення залишається спільним.
const originalUser = {
name: "Olena",
preferences: {
theme: "dark"
}
};
const copiedUser = {
...originalUser
};
copiedUser.preferences.theme = "light";
console.log(originalUser.preferences.theme); // lightУ цьому прикладі originalUser.preferences і copiedUser.preferences посилаються на один вкладений об'єкт.
Схематично:
originalUser.preferences ──┐
├──> { theme: "light" }
copiedUser.preferences ────┘Для незалежної копії вкладеної структури можна використати structuredClone():
const originalUser = {
name: "Olena",
preferences: {
theme: "dark"
}
};
const copiedUser = structuredClone(originalUser);
copiedUser.preferences.theme = "light";
console.log(originalUser.preferences.theme); // dark
console.log(copiedUser.preferences.theme); // lightstructuredClone() підходить для багатьох стандартних структур JavaScript, але не для будь-яких можливих значень. Наприклад, функції не можна клонувати таким способом.
JavaScript завжди передає аргументи у функції за значенням.
Для примітиву функція отримує копію самого значення:
function changeNumber(number) {
number = 100;
}
let value = 10;
changeNumber(value);
console.log(value); // 10Зміна локального параметра не впливає на змінну за межами функції.
Для об'єкта функція також отримує значення, але цим значенням є посилання на об'єкт. Тому функція може змінити властивості початкового об'єкта:
function renameUser(user) {
user.name = "Andrii";
}
const user = {
name: "Olena"
};
renameUser(user);
console.log(user.name); // AndriiТочніше казати не «об'єкт передається за посиланням», а:
Посилання на об'єкт передається за значенням.
Це пояснює, чому функція може змінити властивість об'єкта, але не може замінити об'єкт у змінній виклику простим переприсвоєнням параметра:
function replaceUser(user) {
user = {
name: "Marta"
};
}
const user = {
name: "Olena"
};
replaceUser(user);
console.log(user.name); // OlenaУсередині функції параметр user почав посилатися на новий об'єкт, але зовнішня змінна user залишилася без змін.
"use strict";
function updateProfile(profile) {
profile.age += 1;
profile = {
name: "Інший користувач",
age: 0
};
}
let age = 30;
let copiedAge = age;
copiedAge += 1;
const profile = {
name: "Олена",
age
};
const sameProfile = profile;
const copiedProfile = { ...profile };
updateProfile(profile);
console.log(age); // 30
console.log(copiedAge); // 31
console.log(profile.age); // 31
console.log(sameProfile.age); // 31
console.log(copiedProfile.age); // 30
console.log(profile === sameProfile); // true
console.log(profile === copiedProfile); // falseЩо відбулося:
age — примітивне число, тому copiedAge отримала незалежне значення.
sameProfile посилається на той самий об'єкт, що й profile.
copiedProfile — новий об'єкт із поверхнево скопійованими властивостями.
Функція змінила властивість початкового об'єкта.
Переприсвоєння параметра profile всередині функції не замінило зовнішній об'єкт.
Оператор === не порівнює властивості об'єктів. Якщо потрібно перевірити вміст, необхідно явно визначити правила такого порівняння.
Для простих об'єктів із відомими властивостями можна порівняти їх окремо:
function areUsersEqual(firstUser, secondUser) {
return (
firstUser.name === secondUser.name &&
firstUser.age === secondUser.age
);
}
const firstUser = {
name: "Olena",
age: 28
};
const secondUser = {
name: "Olena",
age: 28
};
console.log(areUsersEqual(firstUser, secondUser)); // trueПеретворення через JSON.stringify() іноді використовують для дуже простих JSON-подібних даних, але це не універсальний спосіб порівняння. Він залежить від порядку властивостей і не зберігає всі типи JavaScript, наприклад undefined, функції, Date та Map.
JavaScript тимчасово обгортає примітиви-рядки, числа та булеві значення у спеціальні об'єкти, коли ми звертаємося до їхніх властивостей або методів:
const text = "hello";
console.log(text.toUpperCase()); // HELLO
console.log(text.length); // 5Це не означає, що text став об'єктом:
console.log(typeof text); // stringНе слід без потреби створювати об'єкти-оболонки вручну:
const primitiveNumber = 10;
const objectNumber = new Number(10);
console.log(typeof primitiveNumber); // number
console.log(typeof objectNumber); // object
console.log(primitiveNumber === objectNumber); // falseЗазвичай потрібно використовувати примітивні значення, а не new Number(), new String() або new Boolean().
console.log({ id: 1 } === { id: 1 }); // falseЦе різні об'єкти. Для порівняння потрібно порівняти їхні властивості або використати відповідний алгоритм глибокого порівняння.
const settings = {
theme: "dark"
};
const anotherSettings = settings;
anotherSettings.theme = "light";
console.log(settings.theme); // lightЯкщо потрібна окрема верхньорівнева копія, використовуйте:
const anotherSettings = {
...settings
};Але пам'ятайте про вкладені об'єкти.
constconst list = [];
list.push("item"); // дозволено
// list = ["new item"]; // помилкаconst захищає прив'язку змінної, а не робить її значення незмінним.
== замість ===console.log(0 == false); // true
console.log(0 === false); // falseНеявне перетворення типів може створити неочікувану поведінку. У більшості випадків використовуйте === та !==.
function replace(value) {
value = {};
}
const object = {
id: 1
};
replace(object);
console.log(object.id); // 1Переприсвоєння локального параметра не змінює зовнішню змінну.
Примітиви — це string, number, bigint, boolean, undefined, symbol і null.
Примітивні значення під час присвоєння копіюються незалежно.
Об'єкти, масиви та функції є об'єктними значеннями.
Під час присвоєння об'єкта копіюється посилання на нього, а не весь об'єкт.
Примітиви порівнюються за значенням.
Об'єкти порівнюються за посиланням.
const забороняє переприсвоєння змінної, але не забороняє змінювати властивості об'єкта.
Spread-синтаксис і Object.assign() створюють поверхневу копію.
Вкладені об'єкти після поверхневого копіювання можуть залишатися спільними.
JavaScript передає всі аргументи у функції за значенням; для об'єкта цим значенням є посилання.
Для уникнення помилкових змін потрібно свідомо обирати між мутацією, поверхневим копіюванням і глибоким копіюванням.