18

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

11

Описание проблемы:

Я заметил, что примитивные типы данных (числа, строки и т.д.) в JavaScript передаются по значению. Однако с объектами ситуация неясна, так как они могут быть переданы как по значению (в этом случае переменная, содержащая объект, фактически является ссылкой на него), так и по ссылке (когда переменная содержит сам объект).

Хотя это не имеет большого значения в конечном счете, мне хотелось бы разобраться, каким образом правильно представить соглашения о передаче аргументов. Существует ли выдержка из спецификации JavaScript, которая определяет, какова должна быть семантика в этом отношении?

5 ответ(ов)

6

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

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

Рассмотрим следующий пример:

function changeObject(x) {
  x = { member: "bar" };
  console.log("в функции changeObject: " + x.member);
}

function changeMember(x) {
  x.member = "bar";
  console.log("в функции changeMember: " + x.member);
}

var x = { member: "foo" };

console.log("перед вызовом changeObject: " + x.member);
changeObject(x);
console.log("после вызова changeObject: " + x.member); /* изменение не сохранилось */

console.log("перед вызовом changeMember: " + x.member);
changeMember(x);
console.log("после вызова changeMember: " + x.member); /* изменение сохранилось */

Вывод будет следующим:

перед вызовом changeObject: foo
в функции changeObject: bar
после вызова changeObject: foo

перед вызовом changeMember: foo
в функции changeMember: bar
после вызова changeMember: bar

Как видно из примера, вызов changeObject не изменяет значение переменной x, тогда как changeMember изменяет свойство объекта, что видно и после выхода из функции. Это и есть ключевое различие между изменением ссылочных типов и изменением самого ссылки.

1

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

Нет способа изменить значение, хранящееся в переменной, переданной в качестве параметра, что было бы возможно, если бы JavaScript поддерживал передачу по ссылке.

1

Мое мнение... Вот как я это понимаю. (Не стесняйтесь исправить меня, если я неправ)

Пришло время забыть всё, что вы знаете о передаче по значению и по ссылке.

Потому что в JavaScript не имеет значения, передается ли что-то по значению, по ссылке или как-то ещё. Важно различать мутацию и присваивание параметров, переданных в функцию.

Давайте я постараюсь объяснить это подробнее. Допустим, у вас есть несколько объектов.

var object1 = {};
var object2 = {};

Что мы сделали, так это "присвоили"... Мы присвоили 2 отдельных пустых объекта переменным "object1" и "object2".

Теперь предположим, что object1 нам нравится больше... Поэтому мы "присваиваем" новую переменную.

var favoriteObject = object1;

Далее, по какой-то причине, мы решаем, что нам больше нравится object2. Так что мы выполняем небольшую переоценку.

favoriteObject = object2;

Ничего не произошло с object1 или object2. Мы не изменили никаких данных. Все, что мы сделали, это переопределили, какой объект является нашим любимым. Важно помнить, что object2 и favoriteObject ссылаются на один и тот же объект. Мы можем изменить этот объект через любую из этих переменных.

object2.name = 'Fred';
console.log(favoriteObject.name) // Выводит Fred
favoriteObject.name = 'Joe';
console.log(object2.name); // Выводит Joe

Теперь давайте взглянем на примитивные типы, такие как строки, например.

var string1 = 'Hello world';
var string2 = 'Goodbye world';

Снова выбираем любимую.

var favoriteString = string1;

Обе наши переменные favoriteString и string1 ссылаются на 'Hello world'. Теперь, что произойдет, если мы захотим изменить favoriteString???

favoriteString = 'Hello everyone';
console.log(favoriteString); // Выводит 'Hello everyone'
console.log(string1); // Выводит 'Hello world'

Упс... Что произошло? Мы не смогли изменить string1, меняя favoriteString... Почему?? Потому что мы не изменили наш строковый объект. Все, что мы сделали, это "ПЕРЕПРИСВОИЛИ" переменной favoriteString новое значение. Это, по сути, отключило её от string1. В предыдущем примере, когда мы переименовали наш объект, мы ничего не присваивали. (Что ж, не самой переменной..., мы действительно присвоили свойству name новую строку.) Вместо этого мы мутировали объект, что сохраняет связь между 2 переменными и базовыми объектами. (Даже если бы мы хотели модифицировать или мутабилизировать сам строковый объект, мы не могли бы этого сделать, потому что строки на самом деле неизменяемы в JavaScript.)

Теперь перейдём к функциям и передаче параметров... Когда вы вызываете функцию и передаете параметр, то, по сути, вы выполняете "присвоение" новой переменной, что работает точно так же, как если бы вы использовали знак равенства (=).

Рассмотрим эти примеры.

var myString = 'hello';

// Присваиваем новой переменной (так же, как при передаче в функцию)
var param1 = myString;
param1 = 'world'; // Переоприсвоение

console.log(myString); // Выводит 'hello'
console.log(param1);   // Выводит 'world'

Теперь то же самое, но с функцией.

function myFunc(param1) {
    param1 = 'world';

    console.log(param1);   // Выводит 'world'
}

var myString = 'hello';
// Вызываем myFunc и присваиваем param1 myString так же, как param1 = myString
myFunc(myString);

console.log(myString); // Выводит 'hello'

Хорошо, теперь давайте приведем несколько примеров с использованием объектов... сначала без функции.

var myObject = {
    firstName: 'Joe',
    lastName: 'Smith'
};

// Присваиваем новой переменной (так же, как при передаче в функцию)
var otherObj = myObject;

// Давайте мутируем наш объект
otherObj.firstName = 'Sue'; // Похоже, Джо решил стать девушкой

console.log(myObject.firstName); // Выводит 'Sue'
console.log(otherObj.firstName); // Выводит 'Sue'

// Теперь переоприсваиваем переменную
otherObj = {
    firstName: 'Jack',
    lastName: 'Frost'
};

// Теперь otherObj и myObject ссылаются на два совершенно разных объекта
// И мутация одного объекта не влияет на другой
console.log(myObject.firstName); // Выводит 'Sue'
console.log(otherObj.firstName); // Выводит 'Jack';

Теперь то же самое, но с вызовом функции.

function myFunc(otherObj) {
    // Давайте мутируем наш объект
    otherObj.firstName = 'Sue';
    console.log(otherObj.firstName); // Выводит 'Sue'

    // Теперь давайте переоприсвоим
    otherObj = {
        firstName: 'Jack',
        lastName: 'Frost'
    };
    console.log(otherObj.firstName); // Выводит 'Jack'

    // Снова otherObj и myObject ссылаются на совершенно разные объекты
    // И мутация одного объекта не волшебным образом не мутирует другой
}

var myObject = {
    firstName: 'Joe',
    lastName: 'Smith'
};

// Вызываем myFunc и присваиваем otherObj myObject так же, как otherObj = myObject
myFunc(myObject);

console.log(myObject.firstName); // Выводит 'Sue', как и раньше

Если вы прочитали этот пост до конца, возможно, теперь у вас есть лучшее понимание того, как работают вызовы функций в JavaScript. Не имеет значения, передается ли что-то по ссылке или по значению... Важно различать присваивание и мутацию.

Каждый раз, когда вы передаете переменную в функцию, вы "присваиваете" то, что называется параметром, точно так же, как если бы вы использовали знак равенства (=).

Всегда помните, что знак равенства (=) означает присваивание. Всегда помните, что передача параметра в функцию в JavaScript также означает присваивание. Это одно и то же, и 2 переменные связаны самым одинаковым образом (что означает, что они не связаны, если не считать, что они присвоены одному и тому же объекту).

Единственный случай, когда "изменение переменной" влияет на другую переменную, - это когда основной объект мутабилизируется (в этом случае вы не изменили переменную, а изменили сам объект).

Нет смысла делать различия между объектами и примитивами, потому что это работает точно так же, как если бы вы не использовали функцию и просто присваивали новой переменной с помощью знака равенства.

Единственная подводная камня возникает, когда имя переменной, которую вы передаете в функцию, совпадает с именем параметра функции. В этом случае вам нужно рассматривать параметр внутри функции как совершенно новую переменную, приватную для этой функции (поскольку это так).

function myFunc(myString) {
    // myString приватная и не влияет на внешнюю переменную
    myString = 'hello';
}

var myString = 'test';
myString = myString; // Ничего не делает, myString все еще 'test';

myFunc(myString);
console.log(myString); // Выводит 'test'
1

Эти фразы/понятия были определены задолго до создания JavaScript и не точно описывают семантику этого языка. Я думаю, что попытки применить их к JS вызывают больше путаницы, чем ясности.

Поэтому не стоит зацикливаться на терминах "передача по ссылке/значению".

Рассмотрим следующее:

  1. Переменные — это указатели на значения.
  2. Переопределение переменной просто перенаправляет этот указатель на новое значение.
  3. Переопределение переменной никогда не повлияет на другие переменные, которые указывали на тот же объект, потому что у каждой переменной есть свой собственный указатель.

Если бы мне пришлось дать этому название, я бы сказал "передача по указателю" — в JS мы не имеем дела с указателями, но подкапотный движок это делает.

// код
var obj = {
    name: 'Fred',
    num: 1
};

// иллюстрация
               'Fred'
              /
             /
(obj) ---- {}
             \
              \
               1
// код
obj.name = 'George';

// иллюстрация
                 'Fred'

(obj) ---- {} ----- 'George'
             \
              \
               1
// код
obj = {};

// иллюстрация
                 'Fred'

(obj)      {} ----- 'George'
  |          \
  |           \
 { }            1
// код
var obj = {
    text: 'Hello world!'
};

/* параметры функции получают свой собственный указатель на 
 * аргументы, которые передаются, так же как и любые другие переменные */
someFunc(obj);

// иллюстрация
(контекст вызова)        (контекст someFunc)
           \             /
            \           /
             \         /
              \       /
               \     /
                 { }
                  |
                  |
                  |
            'Hello world'

Некоторые заключительные комментарии:

  • Фразы "передача по значению/ссылке" используются только для описания поведения языка, а не обязательно фактической реализации. В результате этой абстракции теряются критически важные детали, которые необходимы для адекватного объяснения, что неизбежно приводит к ситуации, когда единственный термин не может адекватно описать фактическое поведение без дополнительных пояснений.
  • Искушение считать, что примитивы подчиняются особым правилам, а объекты — нет, вполне оправдано, но примитивы на самом деле являются просто концом цепочки указателей.
  • В качестве финального примера рассмотрим, почему распространенная попытка очистить массив не работает так, как ожидается.
var a = [1, 2];
var b = a;

a = [];
console.log(b); // [1, 2]
// не работает, потому что `b` все еще указывает на оригинальный массив
0

Когда объект, находящийся вне функции, передается в функцию, это происходит путем передачи ссылки на внешний объект.

Если вы используете эту ссылку для манипуляции с объектом, то внешний объект будет затронут. Однако, если внутри функции вы решите перенаправить ссылку на что-то другое, вы не повлияете на внешний объект, потому что фактически вы просто переназначили ссылку на другой объект, не изменяя сам внешний объект.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь