Рассмотрим этот пример:
function changeStuff(a, b, c)
{
a = a * 10;
b.item = "changed";
c = {item: "changed as well"};
}
var num = 10;
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
changeStuff(num, obj1, obj2);
console.log(num);
console.log(obj1.item);
console.log(obj2.item);
Это производит вывод:
10
changed
unchanged
- Если бы JavaScript передал копию всех значений / объектов, то изменение
obj1.item
не повлияло бы на obj1
вне функции, потому что мы бы изменили копию объекта .
- Если бы это было по ссылке, то все бы изменилось.
num
будет 100
, а obj2.item
будет читать "changed as well"
.
Так что же происходит?
(Обратите внимание, что мы еще не упомянули термин pass-by-value и это было специально).
Во-первых, нам нужна дополнительная концепция: типы значений и ссылочные типы.
На самом деле мы не видим различия этого понятия в JavaScript, но понимание его крайне важно, если мы хотим понять передачу по значению и передачу по ссылке.
Давайте на секунду забудем о функциях, чтобы поговорить о ссылочных типах:
var obj1 = {item: "unchanged"};
var obj2 = obj1;
console.log(obj1.item);
console.log(obj2.item);
obj2.item = "changed";
console.log(obj1.item);
console.log(obj2.item);
Как и следовало ожидать, вы получите результат:
unchanged
unchanged
changed
changed
Это потому, что obj1
и obj2
ссылаются на один и тот же объект. И мы изменили или мутировали этот один объект. Мы просто наблюдаем содержимое внутри этого одного объекта через два разных «пути».
Присвоение переменной obj1
переменной obj2
не копирует и не копирует объект. В противном случае изменение obj2.item
изменило бы только скопированный объект, а не оригинал, и мы бы увидели другой вывод.
Вы, наверное, уже знали это, но это , потому что --- в JavaScript --- объекты являются "ссылочными типами". Это означает, что переменная хранит не сам объект , а ссылку на нашего объекта.
С типами значений это могло бы произойти:
var obj1 = {item: "unchanged"};
var obj2 = obj1; // we would now have two objects
obj2.item = "changed"; // change/mutate the copy
console.log(obj1.item); // would print "unchanged"
console.log(obj2.item); // would print "changed"
Но, как вы знаете, этого не происходит в JavaScript.
Возвращаясь к JavaScript, в obj2 = obj1;
мы что-то копируем: копируем то, что хранит переменная. Что хранит переменная? «Ссылка» на объект.
Так, как это относится к передаче по значению против передачи по ссылке?
При передаче по значению происходит следующее:
Каждый раз, когда мы вызываем функцию, для каждого параметра создается новая переменная, и тогда все, что сохраняется в передаваемых вами переменных, будет копироваться и сохраняться в переменных, созданных для вызова функции. Итак, в передаче по значению бит «значение» означает: мы передаем «значение» внутри переменной.
Давайте посмотрим, что это значит, когда мы используем объекты так, как они ведут себя в JavaScript, с учетом этого псевдокода:
function changeStuff(b, c)
{
...
}
var obj1 = {item: "unchanged"};
var obj2 = {item: "unchanged"};
var obj3 = obj2;
changeStuff(obj1, obj2);
console.log(obj1.item);
console.log(obj2.item);
console.log(obj3.item);
Сначала мы создаем два объекта, obj1
относится к одному, obj2
относится ко второму. Затем мы создаем третью переменную obj3
, которая также относится ко второму объекту. (Можно сказать, что obj3
содержит копию ссылки на второй объект, который хранится в obj2
).
Мы вызываем функцию. Потому что мы используем передачу по значению,
создаются новые переменные b
и c
, затем все, что хранится в переменной obj1
, копируется в b
, а все, что хранится в переменной obj2
, копируется в c
.
Если вы все еще отслеживаете:
obj1
и b
теперь содержат одно и то же (ссылка на первый объект)
obj2
, obj3
и c
содержат одно и то же (ссылка на второй объект)
Функция выполняется, после чего мы печатаем вещи.
Давайте подумаем о том, что может произойти, если наша функция никогда не использует переменные obj1/2/3
, где мы написали ...
.
Мы знаем, что теперь у функции есть способы доступа к нашему первому и второму объектам, а именно через ссылки на эти объекты, хранящиеся в b
и c
соответственно.
Таким образом, функция может изменять эти два объекта по своему усмотрению.
Мы также знаем, что присвоение самой переменной просто меняет то, что хранится внутри этой переменной. Так что changeStuff
может также заставить переменные b
и c
ссылаться на объекты, отличные от того, что они делают в настоящее время, но это не повлияет на наши собственные переменные obj1/2/3
Итак, как только функция будет выполнена и мы запустим наши операторы журнала, мы знаем , что obj1
будет по-прежнему ссылаться на первый созданный нами объект, а obj2
и obj3
будут по-прежнему ссылаясь на второй объект, который мы создали.
console.log(obj1.item);
console.log(obj2.item);
console.log(obj3.item);
Теперь мы знаем, что первая строка может печатать что угодно: changeStuff
имеет ссылку на наш первый объект и может изменить свойство item
этого объекта. А поскольку obj1
по-прежнему ссылается на первый объект, мы напечатаем измененное свойство item
.
Точно такой же аргумент применяется ко второй строке, за исключением того, что сейчас мы говорим о нашем втором объекте.
Для третьей строки мы знаем, что obj3
относится к тому же объекту, что и obj2
. Следовательно, он должен печатать точно так же, как и в предыдущей строке, так как мы печатаем одно и то же , мы просто используем разные способы, чтобы добраться туда.
Что если бы наш гипотетический язык использовал вместо этого ссылку на ссылку?
Когда мы вызываем функцию, мы больше не передаем (копируем) содержимое переменных. (Помните, содержимое - это ссылки на объекты).
Вместо этого мы будем говорить функции: пожалуйста, используйте мои переменные obj1
и obj2
всякий раз, когда вы видите, что b
или c
используется в вашем коде.
Что это значит? Это означает, что когда функция присваивается b
, она фактически присваивает наш obj1
и, соответственно, присваивает obj2
, когда она присваивает c
.
Таким образом, после завершения функции obj3
гарантированно будет по-прежнему ссылаться на наш второй объект, но obj2
можно было бы переназначить любому другому или совершенно новому объекту. Мы понятия не имеем.
Или другими словами: при передаче по ссылке переменные параметров в функциях - это не новые независимые переменные, а "псевдонимы" для переменных вызывающего.
Таким образом, все строки console.log
теперь могут печатать что-то совершенно другое!
Так что же делает JavaScript ? Давайте вспомним наш оригинальный пример в верхней части этого поста.
Функция назначена на a
. Однако num
не изменилось. Мы также присвоили c
, но не увидели влияния этого назначения на obj2
. Так что JavaScript не использовал переход по ссылке .
Вместо этого мы смогли наблюдать изменения наших объектов, но не изменения самих наших переменных. Поэтому JavaScript использовал передаваемое по значению (независимые переменные), и он хранит ссылки внутри наших переменных (ссылочных типов), которые ссылаются на наши объекты. Эти ссылки были скопированы в переменные функций, чтобы они могли изменять наши объекты.
Итак: JavaScript передается по значению
Заключительные мысли : Помните, как при передаче по ссылке мы даже не смотрим на то, что находится внутри переменной? Мы просто говорим «использовать эту переменную» или «использовать эту другую переменную здесь»? В этих предложениях мы ссылаемся на на переменную. Этот, этот другой.
Поэтому, когда вы читаете слово «ссылка» в «передача по ссылке», вы должны думать о , ссылающемся на переменные или ссылки на переменные . Неважно, что внутри переменной.
С другой стороны, «передача по значению» говорит о передаче / копировании того, что на самом деле хранится внутри самой переменной, в новые независимые переменные функции. Таким образом, значение в этом термине говорит о том, что находится внутри переменной. Неважно, что это такое, но так как мы создаем новую переменную, нам нужно «заполнить» ее чем-нибудь.
Тогда у нас есть «ссылочные типы» и «типы значений». Они говорят о том, что на самом деле хранится внутри переменной. «Значение» внутри переменной. С «ссылочными типами» наши вещи не хранятся внутри переменной напрямую. Вместо этого переменная хранит ссылку на нашу вещь . Таким образом, «значение» в переменной является ссылкой на вещь. В JavaScript «вещи» являются объектами. С типами значений вещи хранятся непосредственно внутри переменной. Таким образом, «значение» внутри нашей переменной на самом деле является нашей вещью. Эти типы вещей сохраняются в качестве значения в переменной. Отсюда и «типы значений».
Как только вы поймете, что слова "ссылка" и "значение" могут означать очень разные вещи, в зависимости от того, где они используются в предложении / термине, становится намного проще.
Вот как мы добираемся до печально запутанного: «передача по значению, где значения являются ссылками», которую используют некоторые люди. Он точно описывает, что делает JavaScript. Черт возьми, он также описывает Python, Ruby или Java 2 (речь идет только об объектах). Но это на самом деле ничего не объясняет .
Например, вы, вероятно, согласитесь с тем, что выражение «передача ссылки» звучит очень похоже на «передачу по ссылке». За исключением того, что слово «ссылка» в «передаче ссылки» понимается как «ссылка на некоторый объект» 1 , но в «передаче по ссылке» это не означает этого. Неудивительно, что многие заканчивают тем, что изучают / верят, что некоторые языки используют «переход по ссылке», если такой язык используется, даже если язык не ошибается ... В конце концов, кто бы мог догадаться, что это не так? просто различие без разницы, если вы уже не знаете различие?
Запутывают ли термины? Да! Определенно! Сказать, однако, что JavaScript не выполняет «передачу по значению» и сказать, что он делает «что-то другое» или даже пойти так далеко, чтобы сказать: «давайте просто назовем эту« передачу по ссылке »», медвежью услугу людям. Какая польза от того, что «JavaScript не использует передачу по значению», если нет другого поведения, чтобы сопоставить его? Нет необходимости использовать термины «передача по значению» или «передача по ссылке» или что-то еще при обучении JavaScript. JavaScript делает то, что делает JavaScript.
Примечание: в C # land вы можете определять и создавать объекты, которые являются ссылочными типами, вы можете определять и создавать объекты, которые являются типами значений. Вы можете создавать методы, которые принимают ваши ссылочные типы, используя передачу по значению, или вы можете захотеть использовать передачу по ссылке, по вашему выбору. Вы можете создавать методы, которые принимают ваши типы значений и используют передачу по значению, или вы можете использовать передачу по ссылке. Вы получаете полный спектр комбинаций. В JavaScript вы ничего не можете выбрать. Наконец, что не менее важно, вы можете сделать этот выбор для каждого параметра независимо.
Иногда люди называют поведение, которое использует JavaScript call-by-share .
Просто помните, obj1
и obj2
не являются объектами. Они переменные. Переменные ссылаются на объекты. Несмотря на то, что в разговорной речи мы могли бы сказать, давайте изменим item
из obj1
на obj1.item = "something else"
или подытожим, что как "мы изменили obj1
", всегда неявно понимается, что мы не изменили буквально obj1
(что переменная), но мы изменили объект , на который он ссылается. Использование языка, такого как «Когда мы изменяем объект внутри функции с использованием b.item = "changed"
, это изменение распространяется в вызывающую сторону» также является плохой идеей: распространения изменения не происходит. Нам нужно начать объяснение до модификации: b
и obj1
- это два способа доступа к к одному и тому же объекту . Изменение указанного объекта делает именно это. Какой путь мы используем для доступа к этому объекту , не имеет значения.
1 Таким образом, слово "ссылка" означает , ссылаясь на "ссылки на объекты":)
2 действительно с июня 2019 года. Java добавит поддержку пользовательских типов значений в некоторый момент в будущем
Отказ от ответственности: это было упрощено, чтобы говорить исключительно о передаче вещей в контексте переменных, но должно быть иллюстративным и для других сценариев.