Как передаются переменные по значению или по ссылке
Строки, числа, логические значения передаются в функцию по значению . Иными словами при передаче значения в функцию, эта функция получает копию данного значения. Рассмотрим, что это значит в практическом плане:
function change(x) < x = 2 * x; console.log("x in change:", x); >let n = 10; console.log("n before change:", n); // n before change: 10 change(n); // x in change: 20 console.log("n after change:", n); // n after change: 10
Функция change получает некоторое число и увеличивает его в два раза. При вызове функции change ей передается число n. Однако после вызова функции мы видим, что число n не изменилось, хотя в самой функции произошло увеличение значения параметра. Потому что при вызове функция change получает копию значения переменной n. И любые изменения с этой копией никак не затрагивают саму переменную n. В итоге мы получим следующий вывод в консоли браузера
n before change: 10 x in change: 20 n after change: 10
Передача по ссылке
Объекты и массивы представляют ссылочные типы. То есть переменная или константы, которая представляет объект или массив, по сути хранит ссылку или иными словами указатель, которые указывают на адрес в памяти, где хранится объект. Например:
let bob =< name: "Bob" >;
Переменная bob формально хранит объект, в котором определено одно поле name. Фактически же переменная bob хранит ссылку на объект, который расположен где-то в памяти.
И ссылочные типы — объекты и массивы передаются в функцию по ссылке . То есть функция получает копию ссылки на объект, а не копию самого объекта.
function change(user) < user.name = "Tom"; >let bob =< name: "Bob" >; console.log("before change:", bob.name); // Bob change(bob); console.log("after change:", bob.name); // Tom
В данном случае функция change получает некоторый объект и меняет его свойство name. При вызове этой функции в нее передается значение переменной bob:
change(bob);
Но поскольку переменная bob представляет объект и хранит ссылку на некоторый объект в памяти, то функция change получае копию этой ссылки, которая указывает на тот же объект в памяти, что и переменная bob.
В итоге мы увидим, что после вызова функции изменился оригинальный объект bob, который передавался в функцию.
before change: Bob after change: Tom
Однако если мы попробуем переустановить объект или массив полностью, оригинальное значение не изменится.
function change(user)< // полная переустановка объекта user= < name:"Tom" >; > let bob =< name: "Bob" >; console.log("before change:", bob.name); // Bob change(bob); console.log("after change:", bob.name); // Bob
Почему здесь данные не изменяются? Потому что, как писалось выше, функция получает копию ссылки . То есть при передачи в функцию параметру user значения переменной bob:
change(bob);
Переменная bob и параметр user представляют две разные ссылки, но которые указывают на один и тот же объект.
При присвоении параметру в функции другого объекта:
user= < name:"Tom" >;
ссылка user начиначет указывать на другой объект в памяти. То есть после этого bob и user — две разные ссылки, которые указывают на два разных объекта в памяти.
То же самое касается массивов:
function change(array) < array[0] = 8; >function changeFull(array) < array = [9, 8, 7]; >let numbers = [1, 2, 3]; console.log("before change:", numbers); // [1, 2, 3] change(numbers); console.log("after change:", numbers); // [8, 2, 3] changeFull(numbers); console.log("after changeFull:", numbers); // [8, 2, 3]
Передача по значению и передача по ссылке
Обычно подпрограммы могут передавать аргументы двумя способами. Первый называется передачей по значению. Данный метод копирует содержимое аргумента в формальный параметр подпрограммы. Изменения, сделанные в параметре, не влияют на значение переменной, используемой при вызове.
Передача по ссылке является вторым способом передачи аргументов. В данном методе копируется адрес аргумента. В подпрограмме адрес используется для доступа к настоящему аргументу, используемому при вызове. То есть, изменения, сделанные в параметре, влияют на содержимое переменной, используемой при вызове.
Помимо нескольких исключений, С для передачи аргументов использует передачу по значению. Это означает, что обычно нельзя изменять переменные, используемые при вызове функции. Рассмотрим следующую функцию:
int main(void)
int t=10;
printf(«%d %d», sqr(t), t);
return 0;
>
int sqr (int x) x = x*x; return x;
>
В данном примере значение аргумента, передаваемого в sqr(), 10, копируется в параметр х. Когда происходит присваивание х = х * х, модифицируется только локальная переменная х. Переменная t, используемая при вызове sqr(), по-прежнему содержит значение 10. Следовательно, на экране появится «100 10».
Следует помнить, что только копия аргумента передается в функцию. Все, что происходит в функции, не влияет на переменную, используемую при вызове.
Объекты передаются по ссылке или нет?
Часто можно услышать фразу, что в PHP «объекты всегда передаются по ссылке». На самом деле всё немного сложнее.
Как выглядит работа с ссылками в PHP? Для этого используется специальный синтаксис – перед именем переменной или параметра функции ставится символ амперсанда (&). В том случае, когда амперсанд проставлен в сигнатуре функции – это называют передачей параметра по ссылке. Изменяя такую переменную-параметр внутри функции, после выхода из функции мы обнаружим, что значение поменялось и в месте вызова. Думаю, с этим знакомы все.
А что с объектами? Объекты, переданные внутрь какой-то функции в качестве аргументов, ведут себя точно также – если внутри функции мы меняем внутреннее состояние объекта, то снаружи увидим это изменённое состояние. Отличие в том, что в сигнатуре функции не нужно ставить амперсанд перед параметром-объектом.
Отсюда можно сделать ошибочный вывод, что объекты всегда передаются по ссылке, просто синтаксис попроще, не надо обмазываться амперсандами.
А что, если в сигнатуре функции, принимающей объект всё-таки поставить амперсанд? После небольшой проверки, на первый взгляд, ничего не поменяется! Кажется очередной фрактал плохого дизайна: скалярные типы данных можно передать двумя способами, и по ссылке, и по значению, а объекты всегда по ссылке, при этом амперсанд хочешь ставь, хочешь не ставь.
На самом деле, это заблуждение. Разберёмся что здесь происходит.
Упрощённо, механику можно представить так: когда мы создаём объект с помощью оператора new и присваиваем какой-то переменной, в эту переменную помещается не сам объект, а некий идентификатор объекта, id.
Передавая переменную в качестве аргумента внутрь какой-то функции, мы передаём значение этого идентификатора, т.е. передача происходит по значению . Важно понимать, что значением является не сам объект, а его идентификатор.
Таким образом снаружи функции и внутри мы, имея одинаковое значение идентификатора объекта, работаем с одним и тем же объектом.
Но если внутри функции мы присвоим переменной, например null – повлияет ли это на объект снаружи функции? Никак! Мы обнулили переменную содержащую id объекта внутри функции, но снаружи функции, внешняя переменная всё ещё содержит id объекта и сам объект никуда не делся из памяти.
Теперь ставим в сигнатуре функции амперсанд. В этом случае переменная, содержащая идентификатор объекта, передастся по ссылке. Поменяв такую переменную внутри функции, например, присвоив null , мы обнаружим что поменялась и переменная снаружи – она тоже стала null . А объект в памяти стал ничьим и он будет удалён сборщиком мусора.
Подводим итог.
- Формулировка «объекты всегда передаются по ссылке» не корректна. Впрочем, с практической точки зрения такое упрощение не приводит к проблемам в общении с другими разработчиками, все воспринимают эту фразу примерно одинаково: изменив состояние объекта внутри функции мы увидим это и снаружи.
- Есть существенная разница между синтаксисом с амперсандом и без него. Но на практике не припомню, чтобы мне требовалось использовать вариант с амперсандом перед переменной-объектом.
Хранение по ссылке и по значению
Одно значение можно сохранить как есть, но когда их количество неизвестно, то нужен другой подход.
Время чтения: 10 мин
Открыть/закрыть навигацию по статье
- Кратко
- Примитивные типы данных
- Ссылочные типы данных
- Мутации и неизменяемость
- Аргументы функций
- Егор Огарков советует
- Каким будет значение определённого свойства объекта?
Обновлено 5 августа 2022
Кратко
Скопировать ссылку «Кратко» Скопировано
Для хранения различных значений в переменных мы используем разные типы данных. Однако хранятся эти значения по-разному. Примитивные значения (например, числа или строки) хранятся в переменной как есть, а объекты, массивы и функции — по ссылке на место в памяти.
Представим ситуацию, когда у вас в руках есть ложка из набора, и вы кладёте её в какой-то ящик. Такой простой метафорой можно описать присвоение значения в переменную, если представить ящик как переменную, а ложку как значение. Таким образом можно определить и хранение по значению. В следующий раз, когда вы захотите взять ложку, вы можете открыть тот же самый ящик и получить это значение-ложку.
Теперь представим, что у нас есть другая ложка — это специальная ложка, удобная, но при этом никто не знает, какого она размера и как её правильно хранить. Но для удобства вам хотелось так же использовать ящик. Поэтому, когда вы открыли этот же ящик, то там уже не лежит эта ложка, зато находится записка о том, где эту ложку можно найти.
В итоге, чтобы получить ложку, нужно обратиться по этому «адресу». Предположим, что теперь все такие ложки лежат в специальной «ложечной», которая может их вместить, и только оттуда их можно достать. А потому, чтобы получить ложку вам нужно обратиться по данному адресу. Аналогичным образом мы можем положить записки с тем же адресом и в другие ящики, чтобы каждый, кто обращался к ящику знал где найти ложку. Теперь ваша ложка хранится по ссылке.
В чем же фундаментальное отличие?
Отличий несколько, некоторые могут приводить к неприятным последствиями в нашем коде.
То, как будут храниться данные, жёстко связано с типом данных. Нельзя заставить значение примитивного типа храниться по ссылке, и наоборот.
Для того чтобы понять, как хранятся разные типы данных, заглянем в память компьютера.
Примитивные типы данных
Скопировать ссылку «Примитивные типы данных» Скопировано
Когда мы объявляем переменную и сохраняем в неё примитивное значение, то в память записывается какое-то количество байт, которое описывает это значение. Таким образом можно сказать, что наша переменная уже сразу содержит эти байты.
const seven = 7 // 0b0111const eight = 8 // 0b1000
const seven = 7 // 0b0111 const eight = 8 // 0b1000
Если присвоить какое-то значение переменной в другую, то мы просто скопируем это же количество байт в новое место.
const sevenAgain = seven // 0b0111
const sevenAgain = seven // 0b0111
В итоге все наши переменные можно схематически отобразить таким образом:
Когда мы сравниваем два значения, то у нас по сути произойдёт побайтовое сравнение этих величин.
console.log(seven === sevenAgain)// true
console.log(seven === sevenAgain) // true
console.log(seven === eight)// false
console.log(seven === eight) // false
Из-за того, что все примитивные значения хранятся в небольшом и фиксированном количестве байт, операции над ними выполнять несложно. Такие типы данных называют примитивными. В них входят числа ( number ), строки ( string ), булевы ( boolean ), а так же специальные значения null и undefined .
Ссылочные типы данных
Скопировать ссылку «Ссылочные типы данных» Скопировано
С объектами и другими сложными данными дела обстоят сложнее из-за того, что мы не знаем, какое количество памяти для них понадобится. Во время работы с такой структурой компьютеру необходимо следить за тем, сколько памяти уже есть, сколько понадобится, и выделять новую. Работать с такими данными сложнее. Для этого компьютер отдаёт нам ссылку на место, где данные хранятся, и самостоятельно будет работать с ними по инструкциям, которые мы ему даём. Таким образом в переменную мы получаем лишь ссылку на данные.
const myData = <>
const myData = >
Обратите внимание, что направление стрелки поменялось. Так мы обозначим, что наша переменная ссылается на участок памяти.
☝️ Если сейчас присвоить значение из my Data в другую переменную, то мы скопируем ссылку, а не само значение.
const yourData = myData
const yourData = myData
Такой тип данных называется ссылочным и в него входят объекты, массивы и функции. На самом деле и массивы и функции все они так же являются объектами, но это другая история.
Можно ли в таком случае рассчитывать, что значения будут равными? Конечно, можно! В этом случае сравниваться будут ссылки на объект, а не их содержимое. Потому, если обе переменных указываются на одно и то же, смело можно сказать, что значения равны.
const data = <>const anotherData = data console.log(data === anotherData)// true
const data = > const anotherData = data console.log(data === anotherData) // true
И не стоит забывать, что никакого сравнения по значениям не будет, даже если мы создадим абсолютно одинаковые объекты.
const cat = const dog = // Странно ожидать равность кошки и собаки ¯\_(ツ)_/¯ но теперь мы знаем причинуconsole.log(cat === dog)// false
const cat = name: 'Феликс' > const dog = name: 'Феликс' > // Странно ожидать равность кошки и собаки ¯\_(ツ)_/¯ но теперь мы знаем причину console.log(cat === dog) // false
Однако факт того, что несколько переменных могут ссылаться на один и тот же объект означает в себе и некоторые другие особенности. Если кто-то из двух владельцев ссылки будет изменять объект, то изменения отразятся на всех.
yourData.name = 'Саша'console.log(myData)// myData.name = 'Михаил'console.log(yourData)//
yourData.name = 'Саша' console.log(myData) // myData.name = 'Михаил' console.log(yourData) //
Эта особенность часто становится причиной ошибок при работе со ссылочными типами данных, т.к можно легко забыть или даже не знать, что же ещё ссылается на тот же объект.
Если переменная потеряет ссылку на объект, то изменения уже не будут на него влиять
let user = const admin = user // Переопределение никак не повлияет на admin, потому что мы создали новый объектuser = console.log(admin) // admin.isAdmin = true console.log(user) // console.log(admin) //
let user = name: 'Анна', age: 21 > const admin = user // Переопределение никак не повлияет на admin, потому что мы создали новый объект user = name: 'Иван' > console.log(admin) // admin.isAdmin = true console.log(user) // console.log(admin) //
Мутации и неизменяемость
Скопировать ссылку «Мутации и неизменяемость» Скопировано
Изменение значений у полей объекта, добавление или удаление их отразится на всех, кто владеет ссылкой на этот объект. Такие операции называют мутациями. В современных веб-разработке мутаций стараются избегать, потому что мутирование объектов может приводить к ошибкам, которые очень трудно отследить. Однако если мы твердо уверены, что объект нигде более не используется или чётко контролируем ситуацию, то изменение объекта напрямую гораздо проще.
Если нужно безопасно модифицировать объект, то для начала придётся его скопировать. Скопировать объект можно двумя способами: через Object . assign ( ) или используя спред-синтаксис . . .
const admin = name: 'Анна', age: 21, isAdmin: true,> // Чтобы скопировать через Object.assign() нужно передать пустой объектconst adminCopy = Object.assign(<>, admin) const anotherCopy = . admin,>
const admin = name: 'Анна', age: 21, isAdmin: true, > // Чтобы скопировать через Object.assign() нужно передать пустой объект const adminCopy = Object.assign(>, admin) const anotherCopy = . admin, >
Таким образом будет создана совсем новая сущность, которая будет содержать ровно те же значения. Любые изменения в новом объекте уже не затронут предыдущий.
anotherCopy.age = 30anotherCopy.isAdmin = false console.log(anotherCopy)// console.log(admin)//
anotherCopy.age = 30 anotherCopy.isAdmin = false console.log(anotherCopy) // console.log(admin) //
Здесь стоит внести важную оговорку о вложенных объектах. При копировании объекта указанным способом копируются только поля верхней вложенности (сработает поверхностное копирование). Любые вложенные объекты копируются по ссылке. Их изменение затронет и первоисточник:
const original = b: c: 1, >,> const copy = copy.b.c = 2 // Тоже изменился!console.log(original)// < b: < c: 2 >>
const original = b: c: 1, >, > const copy = . original > copy.b.c = 2 // Тоже изменился! console.log(original) // < b: < c: 2 >>
Изменения можно так же внести при копировании.
const cat = name: 'Феликс', color: 'чёрный', isHomeless: false,> const catInBoots = . cat, name: 'Пушок', hasBoots: true,> console.log(catInBoots)// const redCat = Object.assign(cat, < color: 'рыжий', name: 'Борис' >) console.log(redCat)//
const cat = name: 'Феликс', color: 'чёрный', isHomeless: false, > const catInBoots = . cat, name: 'Пушок', hasBoots: true, > console.log(catInBoots) // const redCat = Object.assign(cat, color: 'рыжий', name: 'Борис' >) console.log(redCat) //
Если каждый раз создавать объект, когда мы вносим изменения, то такие объекты называют иммутабельными (immutable) или неизменяемыми. Результатом любой модификации такого объекта всегда должен быть новый объект, при этом старый никак не изменится.
С массивами, кстати, ситуация точно такая же — если изменять содержимое, то изменения отразятся на всех владельцев ссылки. Для копирования массивов, кроме оператора троеточия, можно использовать метод массива slice ( ) . Методы map ( ) и filter ( ) — они тоже создают новый массив. Причём некоторые другие методы (например sort ( ) , splice ( ) ) при использовании мутируют исходный массив, потому использовать их стоит с осторожностью. Подробнее о том, какой метод мутирует массив можно найти на Does It Mutate.
Очевидным минусом использования иммутабельности может быть большее использование памяти, но в реалиях современной разработки это часто не бывает проблемой, учитывая те плюсы, которые мы получаем.
Аргументы функций
Скопировать ссылку «Аргументы функций» Скопировано
Про тип данных стоит помнить особенно внимательно при использовании функций. Когда мы используем значение как аргумент функции, то все особенности его типа данных сохраняются:
- При передаче примитивного типа данных, его значение копируется в аргумент.
- При использовании ссылочного типа данных копируется ссылка. Все изменения в объекте, который был передан в качестве аргумента, будут видны всем, кто владеет ссылкой:
const member = function makeAdmin(user) user.isAdmin = true return user> const admin = makeAdmin(member) console.log(admin)// console.log(member)// // Это один и тот же объектconsole.log(admin === member)// true
const member = id: '123', name: 'Иван' > function makeAdmin(user) user.isAdmin = true return user > const admin = makeAdmin(member) console.log(admin) // console.log(member) // // Это один и тот же объект console.log(admin === member) // true
Заключение
Скопировать ссылку «Заключение» Скопировано
Итак, что мы узнали?
- Примитивные типы данных (числа, булевы и строки) хранятся и сравниваются по значению. Можно безопасно менять значение переменной и не бояться, что изменится что-то ещё
- Ссылочные типы данных (объекты, массивы) хранятся и сравниваются по ссылке. При этом при сравнении будет учитываться именно факт того, что две переменные ссылаются на один и тот же объект. Даже если два объекта содержат идентичные значения это ни на что не повлияет
- Изменения внутри объекта будут видны всем у кого есть ссылка на этот объект. Прямое изменение данных объекта называется мутирование. Лучше стараться избегать мутации объекта, т.к это может приводить к неочевидным ошибкам
- Чтобы безопасно менять ссылочный тип данных его необходимо предварительно скопировать. Таким образом будет создана другая ссылка и любые изменения не затронут старый объект
На практике
Скопировать ссылку «На практике» Скопировано
Егор Огарков советует
Скопировать ссылку «Егор Огарков советует» Скопировано
При копировании можно изменить и добавить поля, но вот удалить без мутации нельзя
const dog = name: 'Барбос', color: 'чёрный',> const puppy = . dog, // Можно выставить значение undefined, но это не удаление color: undefined,> // А это удалит поле, хоть delete считается мутированием// Но использование его на копии изменит только puppy, dog не будет измененdelete puppy.color
const dog = name: 'Барбос', color: 'чёрный', > const puppy = . dog, // Можно выставить значение undefined, но это не удаление color: undefined, > // А это удалит поле, хоть delete считается мутированием // Но использование его на копии изменит только puppy, dog не будет изменен delete puppy.color
Популярные в веб-разработке библиотеки React и Redux сильно завязаны на иммутабельности данных и практически построены на этом. Подробнее об этом подходе читайте в статье «Организация потоков данных».
На собеседовании
Скопировать ссылку «На собеседовании» Скопировано
- Мутации и неизменяемость