Передача данных в JavaScript: по значению или по ссылке

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Начинающие и промежуточные разработчики, изучающие JavaScript
  • Опытные разработчики, желающие углубить свои знания о передаче данных в JavaScript
  • Преподаватели и наставники, обучающие программированию на JavaScript

    Каждый, кто писал код на JavaScript дольше недели, сталкивался с ситуацией, когда изменение одной переменной неожиданно меняло значение другой. "Почему этот массив вдруг изменился?", "Откуда взялось новое свойство в объекте?" — эти вопросы преследуют разработчиков годами. Корень проблемы часто скрывается в непонимании того, как JavaScript передает данные — по значению или по ссылке. Давайте разберемся в этом раз и навсегда, с понятными примерами и четкими объяснениями. 🔍

Хотите научиться писать безошибочный JavaScript-код с полным пониманием внутренних механизмов языка? Программа Обучение веб-разработке от Skypro погружает вас в тонкости работы с переменными, функциями и объектами. Вы не просто узнаете правила, но и поймете, почему JavaScript работает именно так, что позволит избежать 90% типичных ошибок начинающих разработчиков. Сильное понимание основ — ключ к мастерству в JavaScript!

Что происходит при передаче данных в JavaScript

JavaScript обрабатывает данные по-разному в зависимости от их типа. Эта особенность языка часто становится источником путаницы для новичков и даже опытных разработчиков, пришедших из других языков программирования. 🤔

Для начала, давайте разделим все типы данных в JavaScript на две большие категории:

  • Примитивные типы: строки, числа, boolean, null, undefined, Symbol и BigInt
  • Ссылочные типы: объекты, массивы, функции, Map, Set и другие структуры данных

Главное отличие между ними заключается в том, как они хранятся в памяти и как передаются между переменными и в функции. Давайте посмотрим на это в виде таблицы:

Характеристика Примитивные типы Ссылочные типы
Способ хранения Хранится непосредственно значение Хранится ссылка на значение
При присваивании Копируется само значение Копируется ссылка на объект
При сравнении Сравниваются значения Сравниваются ссылки
Мутабельность Иммутабельны (неизменяемы) Мутабельны (изменяемы)

Когда вы объявляете переменную с примитивным значением, JavaScript создает в памяти "ячейку", содержащую непосредственно это значение. При работе со ссылочными типами, переменная содержит не сам объект, а адрес в памяти, где этот объект находится.

Алексей, frontend-разработчик

Когда я только начинал работать с JavaScript, у меня возникла странная проблема. Я разрабатывал функционал корзины товаров для интернет-магазина. Пользователь мог добавлять продукты, и я сохранял их в массив.

Беда пришла, когда я попытался создать функцию для редактирования товаров:

JS
Скопировать код
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 действует простое правило: примитивы всегда передаются по значению. Это означает, что при каждом присваивании или передаче в функцию создается новая независимая копия значения. 📋

Рассмотрим простой пример:

JS
Скопировать код
let a = 5;
let b = a; // b получает копию значения из a

a = 10; // изменяем a
console.log(b); // 5 (b не изменилось)

Когда мы присваиваем переменной b значение переменной a, JavaScript копирует значение из a в b. После этого переменные существуют независимо друг от друга, и изменение одной не влияет на другую.

То же самое происходит при передаче примитива в функцию:

JS
Скопировать код
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 иммутабельны (неизменяемы). Это означает, что вы не можете изменить существующее примитивное значение — вы можете только создать новое. Даже методы строк, которые кажутся изменяющими строку, на самом деле возвращают новую строку:

JS
Скопировать код
let str = "hello";
let upperStr = str.toUpperCase(); // возвращает новую строку "HELLO"

console.log(str); // "hello" (оригинал не изменился)
console.log(upperStr); // "HELLO" (новая строка)

Передача примитивов по значению делает код более предсказуемым, так как вы можете быть уверены, что функция не изменит ваши исходные данные. Это особенно важно при создании чистых функций и применении функционального подхода в программировании. 🧩

Объекты и массивы: работа со ссылками

В отличие от примитивов, объекты и массивы в JavaScript передаются по ссылке. Это фундаментальное отличие, которое необходимо понимать для эффективной работы с JavaScript. 🔗

Когда вы создаете объект, JavaScript выделяет для него память и сохраняет в переменной не сам объект, а ссылку (адрес) на него в памяти. При присваивании или передаче объекта копируется именно эта ссылка, а не содержимое объекта:

JS
Скопировать код
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 указывают на один и тот же объект в памяти. Изменение свойства через одну переменную сразу видно при обращении через другую.

Такое же поведение наблюдается при передаче объектов в функции:

JS
Скопировать код
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() Передается ссылка на коллекцию

Важно отметить, что сравнение объектов происходит также по ссылкам, а не по содержимому:

JS
Скопировать код
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;

console.log(obj1 === obj2); // false (разные объекты, хоть и с одинаковым содержимым)
console.log(obj1 === obj3); // true (одна и та же ссылка)

Понимание работы со ссылками критически важно для избежания неожиданных побочных эффектов, особенно в более сложных приложениях. В дальнейшем мы рассмотрим, как безопасно копировать объекты, чтобы избежать непреднамеренных изменений. 🛠️

Неочевидные случаи и распространённые ошибки

Даже разработчики с опытом иногда попадают в ловушки, связанные с передачей данных в JavaScript. Рассмотрим наиболее распространённые неочевидные случаи и ошибки. 🚩

Михаил, технический лид

Недавно в нашем проекте произошёл забавный случай. Один из разработчиков добавил функцию для фильтрации статистических данных, и после её вызова основной набор данных приложения начал "магическим" образом изменяться.

Код выглядел примерно так:

JS
Скопировать код
function filterActiveUsers(users) {
return users.filter(user => user.isActive);
}

// Где-то в другой части кода:
const filteredUsers = filterActiveUsers(allUsers);

// Позже в коде:
filteredUsers.forEach(user => {
user.lastVisit = new Date();
});

Мы были удивлены, когда обнаружили, что массив allUsers тоже содержал обновлённые даты последнего визита!

Проблема оказалась в том, что filter() создаёт новый массив, но с теми же ссылками на объекты. Мы думали, что работаем с независимой копией данных, но на самом деле модифицировали исходные объекты. После разбора этого кейса мы внедрили в команде правило: всегда создавать глубокие копии объектов, если планируются их модификации.

Давайте рассмотрим наиболее частые ситуации, которые вызывают путаницу:

1. Методы массивов, создающие новые массивы, но не копирующие объекты внутри

JS
Скопировать код
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. Деструктуризация не создаёт глубоких копий

JS
Скопировать код
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. Сравнение объектов и массивов

JS
Скопировать код
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()
JS
Скопировать код
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. Передача функций в другие функции

JS
Скопировать код
function changeCallback(callback) {
callback.called = true;
return callback;
}

function myCallback() { console.log("Called"); }
changeCallback(myCallback);

console.log(myCallback.called); // true (функции — это объекты)

Избегание этих ловушек требует глубокого понимания механизмов передачи данных в JavaScript и осознанного подхода к работе с мутабельными данными. 🧠

Практические приёмы для безопасной работы с данными

Теперь, когда мы понимаем, как JavaScript обрабатывает различные типы данных, давайте рассмотрим практические приёмы для безопасной работы с ними. Эти методы помогут избежать неожиданных побочных эффектов и сделать ваш код более предсказуемым. 🛡️

1. Создание копий объектов и массивов

Для безопасного изменения объектов и массивов всегда создавайте их копии:

Поверхностное копирование (первый уровень вложенности):

JS
Скопировать код
// 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();

Глубокое копирование (все уровни вложенности):

JS
Скопировать код
// Вариант 1: JSON (с ограничениями – не поддерживает функции, Map, Set, циклические ссылки)
const deepCopy = JSON.parse(JSON.stringify(original));

// Вариант 2: структурное клонирование (в современных браузерах)
const deepCopy2 = structuredClone(original);

// Вариант 3: использование библиотек (lodash, Immer и т.д.)
// const deepCopy3 = _.cloneDeep(original);

2. Иммутабельный подход

Вместо изменения существующих объектов, создавайте новые с нужными изменениями:

JS
Скопировать код
// Плохой подход (мутирование)
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. Защитные техники при работе с функциями

Если вы не уверены, будет ли функция изменять входные параметры, лучше защититься от возможных изменений:

JS
Скопировать код
function processSensitiveData(data) {
// Создаём копию перед обработкой
const safeCopy = Array.isArray(data) 
? [...data]
: (typeof data === 'object' && data !== null)
? { ...data }
: data;

// Теперь безопасно работаем с safeCopy
// ...

return result;
}

5. Использование Object.freeze() для защиты от изменений

Если вы хотите сделать объект полностью неизменяемым (на верхнем уровне):

JS
Скопировать код
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 — не просто теоретическое знание, а практический инструмент, позволяющий писать более надёжный и предсказуемый код. Разделяя в своём мышлении примитивы (всегда копируются) и объекты (передаются по ссылке), вы сможете предотвратить множество трудноуловимых ошибок. Выработайте привычку к иммутабельному стилю программирования — создавайте новые объекты вместо изменения существующих, и ваш код станет более понятным, тестируемым и устойчивым к ошибкам.

Загрузка...