Передача данных в JavaScript: по значению или по ссылке
Для кого эта статья:
- Начинающие и промежуточные разработчики, изучающие JavaScript
- Опытные разработчики, желающие углубить свои знания о передаче данных в JavaScript
Преподаватели и наставники, обучающие программированию на JavaScript
Каждый, кто писал код на JavaScript дольше недели, сталкивался с ситуацией, когда изменение одной переменной неожиданно меняло значение другой. "Почему этот массив вдруг изменился?", "Откуда взялось новое свойство в объекте?" — эти вопросы преследуют разработчиков годами. Корень проблемы часто скрывается в непонимании того, как JavaScript передает данные — по значению или по ссылке. Давайте разберемся в этом раз и навсегда, с понятными примерами и четкими объяснениями. 🔍
Хотите научиться писать безошибочный JavaScript-код с полным пониманием внутренних механизмов языка? Программа Обучение веб-разработке от Skypro погружает вас в тонкости работы с переменными, функциями и объектами. Вы не просто узнаете правила, но и поймете, почему JavaScript работает именно так, что позволит избежать 90% типичных ошибок начинающих разработчиков. Сильное понимание основ — ключ к мастерству в JavaScript!
Что происходит при передаче данных в JavaScript
JavaScript обрабатывает данные по-разному в зависимости от их типа. Эта особенность языка часто становится источником путаницы для новичков и даже опытных разработчиков, пришедших из других языков программирования. 🤔
Для начала, давайте разделим все типы данных в JavaScript на две большие категории:
- Примитивные типы: строки, числа, boolean, null, undefined, Symbol и BigInt
- Ссылочные типы: объекты, массивы, функции, Map, Set и другие структуры данных
Главное отличие между ними заключается в том, как они хранятся в памяти и как передаются между переменными и в функции. Давайте посмотрим на это в виде таблицы:
| Характеристика | Примитивные типы | Ссылочные типы |
|---|---|---|
| Способ хранения | Хранится непосредственно значение | Хранится ссылка на значение |
| При присваивании | Копируется само значение | Копируется ссылка на объект |
| При сравнении | Сравниваются значения | Сравниваются ссылки |
| Мутабельность | Иммутабельны (неизменяемы) | Мутабельны (изменяемы) |
Когда вы объявляете переменную с примитивным значением, JavaScript создает в памяти "ячейку", содержащую непосредственно это значение. При работе со ссылочными типами, переменная содержит не сам объект, а адрес в памяти, где этот объект находится.
Алексей, frontend-разработчик
Когда я только начинал работать с JavaScript, у меня возникла странная проблема. Я разрабатывал функционал корзины товаров для интернет-магазина. Пользователь мог добавлять продукты, и я сохранял их в массив.
Беда пришла, когда я попытался создать функцию для редактирования товаров:
function editProduct(product, newPrice) {
product.price = newPrice;
return product;
}
const apple = { id: 1, name: 'Apple', price: 50 };
const updatedApple = editProduct(apple, 60);
console.log(apple); // { id: 1, name: 'Apple', price: 60 }
console.log(updatedApple); // { id: 1, name: 'Apple', price: 60 }
К моему удивлению, изменился не только возвращаемый объект, но и оригинальный товар в корзине! Пользователь добавлял яблоко по цене 50, а после редактирования оно стоило уже 60 — оригинал изменился. Тогда я и узнал о передаче по ссылке в JavaScript. После изучения этого вопроса я переписал функцию, создавая копию объекта перед изменением.
Понимание того, как JavaScript обрабатывает данные, позволяет предсказуемо управлять состоянием приложения и избегать неожиданных побочных эффектов. Давайте глубже погрузимся в каждый из аспектов передачи данных. ✨

Передача примитивов: копирование значений
При работе с примитивами в JavaScript действует простое правило: примитивы всегда передаются по значению. Это означает, что при каждом присваивании или передаче в функцию создается новая независимая копия значения. 📋
Рассмотрим простой пример:
let a = 5;
let b = a; // b получает копию значения из a
a = 10; // изменяем a
console.log(b); // 5 (b не изменилось)
Когда мы присваиваем переменной b значение переменной a, JavaScript копирует значение из a в b. После этого переменные существуют независимо друг от друга, и изменение одной не влияет на другую.
То же самое происходит при передаче примитива в функцию:
function increaseNumber(num) {
num = num + 1;
return num;
}
let x = 5;
let result = increaseNumber(x);
console.log(x); // 5 (оригинальное значение не изменилось)
console.log(result); // 6 (функция вернула новое значение)
В данном случае функция increaseNumber получает копию значения переменной x. Любые изменения параметра num внутри функции затрагивают только эту локальную копию, но не оригинальную переменную x.
Вот список всех примитивных типов в JavaScript и их поведение при передаче:
- Number: передается копия числового значения
- String: передается копия строки (несмотря на то, что строки могут быть большими)
- Boolean: передается копия логического значения (true или false)
- null: передается копия значения null
- undefined: передается копия значения undefined
- Symbol: передается копия символа
- BigInt: передается копия большого целого числа
Важно помнить, что примитивы в JavaScript иммутабельны (неизменяемы). Это означает, что вы не можете изменить существующее примитивное значение — вы можете только создать новое. Даже методы строк, которые кажутся изменяющими строку, на самом деле возвращают новую строку:
let str = "hello";
let upperStr = str.toUpperCase(); // возвращает новую строку "HELLO"
console.log(str); // "hello" (оригинал не изменился)
console.log(upperStr); // "HELLO" (новая строка)
Передача примитивов по значению делает код более предсказуемым, так как вы можете быть уверены, что функция не изменит ваши исходные данные. Это особенно важно при создании чистых функций и применении функционального подхода в программировании. 🧩
Объекты и массивы: работа со ссылками
В отличие от примитивов, объекты и массивы в JavaScript передаются по ссылке. Это фундаментальное отличие, которое необходимо понимать для эффективной работы с JavaScript. 🔗
Когда вы создаете объект, JavaScript выделяет для него память и сохраняет в переменной не сам объект, а ссылку (адрес) на него в памяти. При присваивании или передаче объекта копируется именно эта ссылка, а не содержимое объекта:
let person1 = { name: "Alice", age: 30 };
let person2 = person1; // person2 получает ссылку на тот же объект
person2.age = 31; // изменяем свойство через person2
console.log(person1.age); // 31 (изменение отразилось и в person1)
console.log(person2.age); // 31
В этом примере переменные person1 и person2 указывают на один и тот же объект в памяти. Изменение свойства через одну переменную сразу видно при обращении через другую.
Такое же поведение наблюдается при передаче объектов в функции:
function celebrateBirthday(person) {
person.age++; // увеличиваем возраст
return person;
}
let john = { name: "John", age: 25 };
celebrateBirthday(john);
console.log(john.age); // 26 (оригинальный объект изменился)
Здесь функция celebrateBirthday получает не копию объекта john, а ссылку на него. Поэтому изменение свойства age внутри функции модифицирует оригинальный объект.
Это поведение распространяется на все ссылочные типы данных в JavaScript:
| Тип данных | Пример | Поведение при передаче |
|---|---|---|
| Объект (Object) | { key: value } | Передается ссылка на объект |
| Массив (Array) | [1, 2, 3] | Передается ссылка на массив |
| Функция (Function) | function() {} | Передается ссылка на функцию |
| Дата (Date) | new Date() | Передается ссылка на объект даты |
| RegExp | /pattern/ | Передается ссылка на регулярное выражение |
| Map, Set | new Map(), new Set() | Передается ссылка на коллекцию |
Важно отметить, что сравнение объектов происходит также по ссылкам, а не по содержимому:
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log(obj1 === obj2); // false (разные объекты, хоть и с одинаковым содержимым)
console.log(obj1 === obj3); // true (одна и та же ссылка)
Понимание работы со ссылками критически важно для избежания неожиданных побочных эффектов, особенно в более сложных приложениях. В дальнейшем мы рассмотрим, как безопасно копировать объекты, чтобы избежать непреднамеренных изменений. 🛠️
Неочевидные случаи и распространённые ошибки
Даже разработчики с опытом иногда попадают в ловушки, связанные с передачей данных в JavaScript. Рассмотрим наиболее распространённые неочевидные случаи и ошибки. 🚩
Михаил, технический лид
Недавно в нашем проекте произошёл забавный случай. Один из разработчиков добавил функцию для фильтрации статистических данных, и после её вызова основной набор данных приложения начал "магическим" образом изменяться.
Код выглядел примерно так:
function filterActiveUsers(users) {
return users.filter(user => user.isActive);
}
// Где-то в другой части кода:
const filteredUsers = filterActiveUsers(allUsers);
// Позже в коде:
filteredUsers.forEach(user => {
user.lastVisit = new Date();
});
Мы были удивлены, когда обнаружили, что массив
allUsersтоже содержал обновлённые даты последнего визита!
Проблема оказалась в том, что
filter()создаёт новый массив, но с теми же ссылками на объекты. Мы думали, что работаем с независимой копией данных, но на самом деле модифицировали исходные объекты. После разбора этого кейса мы внедрили в команде правило: всегда создавать глубокие копии объектов, если планируются их модификации.
Давайте рассмотрим наиболее частые ситуации, которые вызывают путаницу:
1. Методы массивов, создающие новые массивы, но не копирующие объекты внутри
const originalArray = [{ name: "Alice" }, { name: "Bob" }];
const newArray = originalArray.map(person => person); // новый массив, но те же объекты
newArray[0].name = "Alicia";
console.log(originalArray[0].name); // "Alicia" (объект изменился)
Методы map(), filter(), slice() создают новый массив, но объекты внутри остаются теми же самыми. Это называется "поверхностное копирование" (shallow copy).
2. Деструктуризация не создаёт глубоких копий
const user = { name: "John", profile: { age: 25 } };
const { ...userCopy } = user;
userCopy.name = "Jack"; // Это не повлияет на original.name
userCopy.profile.age = 26; // А это изменит original.profile.age!
console.log(user.profile.age); // 26
Деструктуризация и spread-оператор создают только поверхностные копии объектов. Вложенные объекты по-прежнему передаются по ссылке.
3. Сравнение объектов и массивов
const array1 = [1, 2, 3];
const array2 = [1, 2, 3];
console.log(array1 === array2); // false (разные ссылки, хоть содержимое и идентично)
// Ещё более неочевидный пример:
const obj = {};
const arr = [];
obj.prop = arr;
arr.push(obj);
// Теперь obj и arr содержат циклические ссылки друг на друга
4. Неизменяемые методы и изменяемые объекты
Многие методы строк кажутся изменяющими, но на самом деле возвращают новую строку. В то же время, многие методы массивов изменяют оригинальный массив:
- Неизменяющие методы строк:
toUpperCase(),trim(),substring() - Изменяющие методы массивов:
push(),pop(),sort(),splice() - Неизменяющие методы массивов:
concat(),slice(),map()
const arr = [3, 1, 2];
const sorted = arr.sort(); // arr изменился и стал [1, 2, 3]
console.log(sorted === arr); // true (sort изменяет оригинал и возвращает его)
const str = "hello";
const upper = str.toUpperCase(); // создаётся новая строка
console.log(upper === str); // false (строки — примитивы, создаётся новая)
5. Передача функций в другие функции
function changeCallback(callback) {
callback.called = true;
return callback;
}
function myCallback() { console.log("Called"); }
changeCallback(myCallback);
console.log(myCallback.called); // true (функции — это объекты)
Избегание этих ловушек требует глубокого понимания механизмов передачи данных в JavaScript и осознанного подхода к работе с мутабельными данными. 🧠
Практические приёмы для безопасной работы с данными
Теперь, когда мы понимаем, как JavaScript обрабатывает различные типы данных, давайте рассмотрим практические приёмы для безопасной работы с ними. Эти методы помогут избежать неожиданных побочных эффектов и сделать ваш код более предсказуемым. 🛡️
1. Создание копий объектов и массивов
Для безопасного изменения объектов и массивов всегда создавайте их копии:
Поверхностное копирование (первый уровень вложенности):
// Spread-оператор для объектов
const original = { name: "John", age: 25 };
const copy = { ...original };
copy.name = "Jack";
console.log(original.name); // "John" (не изменился)
// Object.assign() для объектов
const copy2 = Object.assign({}, original);
// Spread-оператор для массивов
const originalArray = [1, 2, 3];
const arrayCopy = [...originalArray];
// Array.slice() для массивов
const arrayCopy2 = originalArray.slice();
Глубокое копирование (все уровни вложенности):
// Вариант 1: JSON (с ограничениями – не поддерживает функции, Map, Set, циклические ссылки)
const deepCopy = JSON.parse(JSON.stringify(original));
// Вариант 2: структурное клонирование (в современных браузерах)
const deepCopy2 = structuredClone(original);
// Вариант 3: использование библиотек (lodash, Immer и т.д.)
// const deepCopy3 = _.cloneDeep(original);
2. Иммутабельный подход
Вместо изменения существующих объектов, создавайте новые с нужными изменениями:
// Плохой подход (мутирование)
function updateUser(user, newName) {
user.name = newName;
return user;
}
// Хороший подход (иммутабельность)
function updateUser(user, newName) {
return { ...user, name: newName };
}
// Обновление вложенных свойств
function updateNestedProperty(obj, path, value) {
const copy = { ...obj };
const pathArray = path.split('.');
let current = copy;
for (let i = 0; i < pathArray.length – 1; i++) {
const key = pathArray[i];
current[key] = { ...current[key] };
current = current[key];
}
current[pathArray[pathArray.length – 1]] = value;
return copy;
}
const user = {
name: "John",
profile: { age: 25, address: { city: "New York" } }
};
const updated = updateNestedProperty(user, "profile.address.city", "Boston");
3. Использование библиотек для иммутабельных данных
Для сложных объектов рассмотрите использование специализированных библиотек:
- Immer: позволяет удобно работать с иммутабельными данными, используя мутабельный синтаксис
- Immutable.js: предоставляет эффективные иммутабельные структуры данных
- Lodash: содержит функции для глубокого клонирования и безопасной работы с объектами
4. Защитные техники при работе с функциями
Если вы не уверены, будет ли функция изменять входные параметры, лучше защититься от возможных изменений:
function processSensitiveData(data) {
// Создаём копию перед обработкой
const safeCopy = Array.isArray(data)
? [...data]
: (typeof data === 'object' && data !== null)
? { ...data }
: data;
// Теперь безопасно работаем с safeCopy
// ...
return result;
}
5. Использование Object.freeze() для защиты от изменений
Если вы хотите сделать объект полностью неизменяемым (на верхнем уровне):
const config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 3000
});
// Попытка изменения не сработает (в строгом режиме выбросит ошибку)
config.timeout = 5000; // Изменение будет проигнорировано
console.log(config.timeout); // 3000
Применение этих практик поможет вам писать более надёжный и предсказуемый код. Они особенно важны в больших приложениях, где данные проходят через множество функций и компонентов. 🏆
Таблица решений для распространённых проблем с передачей данных:
| Проблема | Решение |
|---|---|
| Непреднамеренное изменение объекта в функции | Создавайте копию объекта в начале функции с помощью spread-оператора |
| Изменение вложенных объектов после копирования | Используйте глубокое клонирование (structuredClone или библиотеки) |
| Необходимость обновить свойство без изменения оригинала | Возвращайте новый объект с обновлённым свойством: { ...obj, prop: newValue } |
| Изменение массива с сохранением порядка элементов | Используйте неизменяющие методы массивов или создавайте копии перед использованием изменяющих методов |
| Случайное изменение общих объектов в компонентах | Внедрите state-менеджмент с поддержкой иммутабельности (Redux, MobX и т.д.) |
Понимание принципов передачи данных в JavaScript — не просто теоретическое знание, а практический инструмент, позволяющий писать более надёжный и предсказуемый код. Разделяя в своём мышлении примитивы (всегда копируются) и объекты (передаются по ссылке), вы сможете предотвратить множество трудноуловимых ошибок. Выработайте привычку к иммутабельному стилю программирования — создавайте новые объекты вместо изменения существующих, и ваш код станет более понятным, тестируемым и устойчивым к ошибкам.