Я покажу, почему фольклор о том, что JS-объекты передаются по ссылке, примитивы передаются по значению не имеет ничего общего ни с передачей, ни с объектами, ни со ссылками и на самом деле является иллюзией. Объекты передаются по ссылке (OAPBR) — это теория/ментальная модель. Как и большинство теорий, о ней следует судить по ее вкладу в понимание, предсказание, контроль и ее стоимости с точки зрения сложности. Я надеюсь использовать этот пример как доказательство того, что очень неправильная ментальная модель:

  • Усложняет изучение расширенных функций
  • Усложняет изучение других языков программирования
  • И тратит время. В конце я дам советы, как избежать очень неправильных ментальных моделей.

Феномен, который нужно объяснить

Чтобы показать, что пытается объяснить теория «Объекты передаются по ссылке» (OAPBR), нам сначала понадобятся два определения:

function plusNum(n1, n2) {
 return n1 + n2;
}

function mutatingAdd(obj1, n) {
 obj1.value = plusNum(obj1.value, n)
}

Далее objectExample передает x takeNum. takeNum добавляет 1, что не влияет на x objectExample:

function takeNum(n /* number */) {
 n = plusNum(n, 1);
 console.log(n); // 2
}

function primitiveExample() {
 let x = 1;
 takeNum(x);
 console.log(x); // 1
}

Теоретик OAPBR сказал бы, что primitiveExample не видит изменения на x в takeNum, потому что примитивы передаются по значению. Мы можем написать внешне похожий код, используя объекты, а не примитив (например, number), и получить то, что кажется отличным поведением:

function mutateObj(obj /* {field: number} */) {
 mutatingAdd(obj, 1);
 console.log(obj.value); // 2
}

function objectExample() {
 let x = {value: 1};
 mutateObj(x);
 console.log(x.value); // 2
}

Теоретик OAPBR сказал бы, что objectExample видит изменение на x в mutateObj, потому что примитивы передаются по значению.

Почему теория «объекты передаются по ссылке» (OAPBR) терпит неудачу

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

function primitiveExample2() {
 let x = 1;
 let y = x;
 y = y + 1;
 console.log(y); // 2
 console.log(x); // 1
}

function objectExample2() {
 let x = { value: 1 };
 let y = x;
 y.value = y.value + 1;
 console.log(y.value); // 2
 console.log(x.value); // 2
}

Таким образом, теория «объекты передаются по ссылке, примитивы передаются по значению» уже не может обобщить, чтобы объяснить, что происходит, когда вы присваиваете значения переменным. Мы можем добавить эпициклы к теории OAPBR, чтобы объяснить это, но это еще хуже. Мало того, что объясняемое явление не имеет ничего общего с переходными (вызывающими функциями), оно не имеет ничего общего с объектами. Здесь мы видим так называемое поведение «передача по ссылке» и поведение «передача по значению» для одного и того же значения, на которое указывает y:

function combinedExample1() {
 let x = Object.assign(1, { value: 100 });
 let y = x;
 mutatingAdd(y, 100);
 console.log(y.value); // 200
 console.log(x.value); // 200
 y = plusNum(y, 1);
 console.log(y + 0); // 2
 console.log(x + 0); // 1
}

В терминологии теоретика OAPBR y передал как по ссылке, так и по значению. Так это объект или примитив? В примере задействованы некоторые шаткие функции JS, которые я не буду рассматривать, но, как я уже говорил, очень неправильная ментальная модель может затруднить понимание расширенных функций. Верующий в OAPBR на этом этапе может добавить больше эпициклов, быть очень взволнованным открытием JavaScript-эквивалента корпускулярно-волнового дуализма и начать добавлять эпициклы в свою теорию. Но мы можем сэкономить время теоретика OAPBR, показав на более простом примере, что псевдоразличение «по ссылке против по значению» не имеет ничего общего с объектами и примитивами. Здесь x и y оба указывают на простые объекты JavaScript, но мы наблюдаем только поведение "по значению":

function nonMutatingPlusObj(obj1, obj2) {
 return { value: obj1.value + obj2.value };
}

function nonMutatingObjectExample() {
 let x = {value: 100};
 let y = x;
 y = nonMutatingPlusObj(y, {value: 100})
 console.log(y.value); // 200
 console.log(x.value); // 100
}

Наконец, я сказал, что одна из издержек плохих ментальных моделей заключается в том, что они усложняют изучение других языков программирования. Теория «объекты передаются по ссылке, примитивы передаются по значению» делает загадкой следующий PHP-код. Потому что в PHP-коде впервые в этом посте что-то фактически передается по ссылке. И это целое число:

<?php

function add_mut(&$i1, $i2) {
 $i1 += $i2;
}

$x = 1;
add_mut($x, 1);
echo $x; // 2
?>

Это потому, что PHP имеет фактическую передачу по ссылке, но JS не имеет функции передачи по ссылке. Теория OAPBR также натыкается на понимание этого Rust, где ссылка на целое число передается в add_mut;

fn add_mut(i: &mut i32, i2: i32) {
 *i = (*i) + i2;
}

fn main() {
 let i = &mut 1;
 add_mut(i, 1);
 println!("{}", i); // 2
}

В Rust есть встроенные ссылки на уровне исходного кода, а в JS их нет. Однако вы можете сделать свои собственные ссылки в JS:

function makeRef(thing) {
 return {value: thing};
}

function addMut(iRef, i2) {
 iRef.value = iRef.value + i2;
}

let iRef = makeRef(1);
addMut(iRef, 1);
console.log(iRef.value); // 2

По сути, это как работают Reactrefs.

Лучшая теория

Вместо «Объекты передаются по ссылке, примитивы передаются по значению» я предлагаю лучше различать: между изменением области действия и изменением значения. В JS области действия — это изменяемые сопоставления имен переменных со значениями.

let x = 1; // the scope maps x to 1
x = 2; // the scope maps x to 2
x += 1; // the scope maps x to 3

В приведенном выше коде не изменяется ни одно число (вы не можете изменять numbers в JS), но меняется область действия.

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

В приведенном ниже коде x и y указывают на один и тот же объект, который mutateObj мутирует. mutatingAdd не меняет область видимости, так как функции JavaScript не могут изменять область видимости, которую они не закрывают:

function objectExample() {
  let x = {value: 1}; 
  let y = x;
  mutatingAdd(y, 1);
  console.log(y.value); // 2
  console.log(x.value); // 2
}

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

В частности, для JS понимание того, что область действия является изменяемой, необходимо для понимания const-vs-let, затенения, идентификации объектов, замыканий и написания пользовательских интерфейсов. И это такая простая концепция по сравнению с неправильным различием «по значению против по ссылке».

Что дальше

Я видел плохие ментальные модели для языков программирования из следующих источников:

  • Не используя никаких учебных ресурсов, а вместо этого погружаясь и пытаясь программировать. Это может показаться быстрым, но я думаю, что в долгосрочной перспективе это будет медленный путь.
  • Обучение из сообщений в блогах или видео. (Я знаю, что это запись в блоге: доверяйте MDN больше всего здесь.)
  • Предполагая, что особенности языка программирования очевидны. Ничто не очевидно, особенно переменные, и между языками программирования и внутри них существуют удивительные различия в том, как все работает.

Что мне помогло, в зависимости от языка, так это чтение официальной документации по языку программирования или хорошей книги и/или спецификации. (JS — это частный случай, когда MDN — самый читабельный и точный ресурс). Формирование более точной мысленной модели экономит время по сравнению с альтернативами.

Удачного взлома!