Перейти к основному содержимому

[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 передаёт все аргументы по значению. Для объектов копируемым значением является ссылка (адрес в памяти).

  • Примитивные аргументы: копирование значения не влияет на внешнюю переменную.
  • Аргументы-объекты: копирование ссылки позволяет мутировать один и тот же объект.
  • Переприсваивание локального параметра не изменяет внешнюю привязку.

Ссылки (Reference)​