[Medium] 📄 Примитивные и ссылочные типы
1. Что такое примитивные и ссылочные типы (Primitive Types и Reference Types)?
Что такое примитивные и ссылочные типы?
Типы данных JavaScript можно разделить на две категории: примитивные типы и ссылочные типы. Они принципиально различаются в поведении памяти и семантике передачи.
Примитивные типы (Primitive Types)
Характеристики:
- Хранятся как непосредственные значения (концептуально в стеке)
- Передаются копированием значения
- Неизменяемы (immutable)
7 примитивных типов:
// 1. String
const str = 'hello';
// 2. Number
const num = 42;
// 3. Boolean
const bool = true;
// 4. Undefined
let undef;
// 5. Null
const n = null;
// 6. Symbol (ES6)
const sym = Symbol('unique');
// 7. BigInt (ES2020)
const bigInt = 9007199254740991n;
Ссылочные типы (Reference Types)
Характеристики:
- Объекты размещаются в куче (heap)
- Переменные хранят ссылки (адреса)
- Изменяемы (mutable)
Примеры:
// 1. Object
const obj = { name: 'John' };
// 2. Array
const arr = [1, 2, 3];
// 3. Function
const func = function () {};
// 4. Date
const date = new Date();
// 5. RegExp
const regex = /abc/;
// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();
2. Передача по значению vs передача по ссылке (Call by Value vs Call by Reference)
Передача по значению vs передача по ссылке
Передача по значению (поведение примитивов)
Поведение: значение копируется; изменение копии не влияет на оригинал.
let a = 10;
let b = a; // копирование значения
b = 20;
console.log(a); // 10
console.log(b); // 20
Диаграмма памяти:
┌─────────┐
│ Стек │
├─────────┤
│ a: 10 │ <- независимое значение
├─────────┤
│ b: 20 │ <- независимое значение после копирования/обновления
└─────────┘
Поведение ссылок (объекты)
Поведение: копируется ссылка; обе переменные могут указывать на один и тот же объект.
let obj1 = { name: 'John' };
let obj2 = obj1; // копирование ссылки
obj2.name = 'Jane';
console.log(obj1.name); // 'Jane'
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true
Диаграмма памяти:
┌─────────┐ ┌──────────────────┐
│ Стек │ │ Куча │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (тот же объект) │
└─────────┘ └──────────────────┘
3. Распространённые задачи (Common Quiz Questions)
Распространённые задачи
Задача 1: передача примитивных значений
function changeValue(x) {
x = 100;
console.log('Inside function x:', x);
}
let num = 50;
changeValue(num);
console.log('Outside function num:', num);
Нажмите, чтобы увидеть ответ
// Inside function x: 100
// Outside function num: 50
Объяснение:
num— примитив (Number)- аргумент функции получает скопированное значение
- изменение
xне влияет наnum
// процесс
let num = 50; // Стек: num = 50
changeValue(num); // Стек: x = 50 (копия)
x = 100; // изменяется только x
console.log(num); // всё ещё 50
Задача 2: передача объектов
function changeObject(obj) {
obj.name = 'Changed';
console.log('Inside function obj.name:', obj.name);
}
let person = { name: 'Original' };
changeObject(person);
console.log('Outside function person.name:', person.name);
Нажмите, чтобы увидеть ответ
// Inside function obj.name: Changed
// Outside function person.name: Changed
Объяснение:
person— ссылочный тип (Object)- аргумент функции копирует ссылку
objиpersonуказывают на один и тот же объект
// схема памяти
let person = { name: 'Original' }; // куча @0x001
changeObject(person); // obj -> @0x001
obj.name = 'Changed'; // мутация @0x001
console.log(person.name); // чтение из @0x001
Задача 3: переприсваивание vs мутация свойства
function test1(obj) {
obj.name = 'Modified'; // мутация свойства
}
function test2(obj) {
obj = { name: 'New Object' }; // переприсваивание локального параметра
}
let person = { name: 'Original' };
test1(person);
console.log('A:', person.name);
test2(person);
console.log('B:', person.name);
Нажмите, чтобы увидеть ответ
// A: Modified
// B: Modified (не 'New Object')
Объяснение:
test1: мутация свойства
function test1(obj) {
obj.name = 'Modified'; // мутирует оригинальный объект
}
test2: переприсваивание
function test2(obj) {
obj = { name: 'New Object' }; // изменяет только локальную привязку
}
// person по-прежнему указывает на оригинальный объект
Схема памяти:
// до test1
person ---> { name: 'Original' }
obj ---> { name: 'Original' } (тот же)
// после test1
person ---> { name: 'Modified' }
obj ---> { name: 'Modified' } (тот же)
// внутри test2
person ---> { name: 'Modified' } (не изменился)
obj ---> { name: 'New Object' } (новый объект)
// после test2
person ---> { name: 'Modified' }
// локальный obj удалён
Задача 4: передача массива
function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}
function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}
let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);
reassignArray(numbers);
console.log('4:', numbers);
Нажмите, чтобы увидеть ответ
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]
Объяснение:
modifyArray: мутирует оригинальный массивreassignArray: только перепривязывает локальный параметр
Задача 5: сравнение на равенство
// примитивы
let a = 10;
let b = 10;
console.log('A:', a === b);
// ссылки
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
Нажмите, чтобы увидеть ответ
// A: true
// B: false
// C: true
Объяснение:
Примитивы сравниваются по значению; объекты сравниваются по ссылке.
obj1 === obj2; // false (разные ссылки)
obj1 === obj3; // true (одна ссылка)
4. Поверхностное копирование vs глубокое копирование (Shallow Copy vs Deep Copy)
Поверхностное копирование vs глубокое копирование
Поверхностное копирование (Shallow Copy)
Определение: копируется только верхний уровень; вложенные объекты остаются общими ссылками.
Способ 1: оператор spread
const original = {
name: 'John',
address: { city: 'Taipei' },
};
const copy = { ...original };
copy.name = 'Jane';
console.log(original.name); // 'John'
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung' (затронут)
Способ 2: Object.assign()
const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);
copy.name = 'Jane';
console.log(original.name); // 'John'
Способ 3: поверхностное копирование массива
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
const arr3 = arr1.slice();
const arr4 = Array.from(arr1);
arr2[0] = 999;
console.log(arr1[0]); // 1
Глубокое копирование (Deep Copy)
Определение: все уровни копируются рекурсивно.
Способ 1: JSON.parse(JSON.stringify(...))
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};
const copy = JSON.parse(JSON.stringify(original));
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'
copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming']
Ограничения:
const obj = {
date: new Date(), // -> строка
func: () => {}, // игнорируется
undef: undefined, // игнорируется
symbol: Symbol('test'), // игнорируется
regexp: /abc/, // -> {}
circular: null, // циклическая ссылка выбрасывает ошибку
};
obj.circular = obj;
JSON.parse(JSON.stringify(obj)); // ошибка или потеря данных
Способ 2: structuredClone()
const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};
const copy = structuredClone(original);
console.log(copy.date instanceof Date); // true
Плюсы:
- Поддерживает Date, RegExp, Map, Set и т.д.
- Поддерживает циклические ссылки
- Обычно лучшая производительность, чем ручное глубокое клонирование
Ограничения:
- Не клонирует функции
- Не клонирует значения Symbol во всех паттернах использования
Способ 3: рекурсивное глубокое клонирование
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}
if (obj instanceof Date) {
return new Date(obj);
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};
const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'
Способ 4: Lodash
import _ from 'lodash';
const original = {
name: 'John',
address: { city: 'Taipei' },
};
const copy = _.cloneDeep(original);
Сравнение поверхностного и глубокого копирования
| Характеристика | Поверхностное копирование | Глубокое копирование |
|---|---|---|
| Глубина копирования | Только верхний уровень | Все уровни |
| Вложенные объекты | Общие ссылки | Полностью независимые |
| Производительность | Быстрее | Медленнее |
| Использование памяти | Ниже | Выше |
| Случай использования | Простые структуры | Сложные вложенные структуры |
5. Распространённые подводные камни (Common Pitfalls)
Распространённые подводные камни
Подводный камень 1: ожидание мутации внешнего значения через примитивный аргумент
function increment(num) {
num = num + 1;
return num;
}
let count = 5;
increment(count);
console.log(count); // 5
count = increment(count);
console.log(count); // 6
Подводный камень 2: ожидание замены внешнего объекта через переприсваивание
function resetObject(obj) {
obj = { name: 'Reset' }; // только локальная перепривязка
}
let person = { name: 'Original' };
resetObject(person);
console.log(person.name); // 'Original'
// правильный подход 1: мутировать свойство
function resetObject1(obj) {
obj.name = 'Reset';
}
// правильный подход 2: вернуть новый объект
function resetObject2(obj) {
return { name: 'Reset' };
}
person = resetObject2(person);
Подводный камень 3: предположение, что spread — это глубокое копирование
const original = {
user: { name: 'John' },
};
const copy = { ...original }; // поверхностное
copy.user.name = 'Jane';
console.log(original.user.name); // 'Jane'
const deep = structuredClone(original);
Подводный камень 4: непонимание const
const obj = { name: 'John' };
// obj = { name: 'Jane' }; // TypeError
obj.name = 'Jane'; // допустимо
obj.age = 30; // допустимо
const immutableObj = Object.freeze({ name: 'John' });
immutableObj.name = 'Jane';
console.log(immutableObj.name); // 'John'
Подводный камень 5: общая ссылка в циклах
const arr = [];
const obj = { value: 0 };
for (let i = 0; i < 3; i++) {
obj.value = i;
arr.push(obj); // одна и та же ссылка на объект каждый раз
}
console.log(arr);
// [{ value: 2 }, { value: 2 }, { value: 2 }]
const arr2 = [];
for (let i = 0; i < 3; i++) {
arr2.push({ value: i }); // новый объект на каждой итерации
}
console.log(arr2);
// [{ value: 0 }, { value: 1 }, { value: 2 }]
6. Лучшие практики (Best Practices)
Лучшие практики
✅ Рекомендуется
// 1. выбирайте явную стратегию копирования
const original = { name: 'John', age: 30 };
const copy1 = { ...original }; // поверхностное
const copy2 = structuredClone(original); // глубокое
// 2. избегайте побочных эффектов мутации в функциях
function addItem(arr, item) {
return [...arr, item]; // иммутабельный стиль
}
// 3. используйте const для предотвращения случайной перепривязки
const config = { theme: 'dark' };
// 4. используйте Object.freeze для неизменяемых констант
const constants = Object.freeze({
PI: 3.14159,
MAX_SIZE: 100,
});
❌ Избегайте
function increment(num) {
num++; // не влияет на внешний примитив
}
const copy = { ...nested }; // не глубокое копирование
for (let i = 0; i < 3; i++) {
arr.push(obj); // одна и та же ссылка на объект используется повторно
}
7. Итоги для собеседования (Interview Summary)
Итоги для собеседования
Краткая шпаргалка
Примитивные (Primitive):
- String, Number, Boolean, Undefined, Null, Symbol, BigInt
- Передаются по значению
- Неизменяемы
Ссылочные (Reference):
- Object, Array, Function, Date, RegExp и т.д.
- Переменная хранит ссылку на объект в куче
- Изменяемы
Пример ответа на собеседовании
В: JavaScript — это передача по значению или по ссылке?
JavaScript передаёт все аргументы по значению. Для объектов копируемым значением является ссылка (адрес в памяти).
- Примитивные аргументы: копирование значения не влияет на внешнюю переменную.
- Аргументы-объекты: копирование ссылки позволяет мутировать один и тот же объект.
- Переприсваивание локального параметра не изменяет внешнюю привязку.