Ссылочные типы данных — Java: Введение в ООП
Классы в Java особым образом связаны с типами данных. Посмотрите на пример:
var user = new User("Danil", "Miloshin");
Каким будет реальный тип в данном случае? Классы, сами по себе, ведут себя как типы. Поэтому типом переменной user будет User , то есть так:
User user = new User("Danil", "Miloshin");
В Java все типы данных делятся на две категории: примитивные и ссылочные. К примитивным относятся все виды чисел, символы и логический тип данных (булеан). К ссылочным — классы, массивы, строки. В зависимости от категории, значительно меняется поведение кода и об этом нужно знать. В этом уроке мы разберем отличия между этими категориями и научимся правильно с ними работать.
Для изучения нам понадобится пример какого-то класса, чьи объекты мы используем в примерах кода. Возьмем для простоты класс User с двумя полями и одним конструктором:
class User public String firstName; public String lastName; public User(String firstName, String lastName) this.firstName = firstName; this.lastName = lastName; > >
Значение по умолчанию
Примитивные данные всегда имеют значение, даже если они определяются без инициализации:
int a; System.out.println(a); // => 0
У ссылочных в качестве значения по умолчанию используется null . Это специальное значение, которое может быть использовано в качестве любого объекта
User u; System.out.println(u); // => null // Можно присваивать и явно // User u = null;
Присваивание
Примитивное значение всегда копируется при присваивании:
// Содержимое a и b не связаны var a = 5; var b = a;
Ссылочные же данные не копируются. При присваивании переменные начинают указывать (ссылаться) на один и тот же объект:
var u1 = new User("Igor", "Mon"); // Обе переменные ссылаются на один тот же объект var u2 = u1; u2.firstName = "Nina"; System.out.println(u1.firstName); // => Nina u1 == u2; // true // u2 теперь ссылается на другой объект // Содержимое объекта при этом не важно, оно может быть одинаковым, а может быть разным // Java проверяет только то, та же ли это ссылка или нет u2 = new User("Igor", "Mon"); u1 == u2; // false
Больше всего это проявляется при передаче данных в методы и их возврате оттуда. Ссылочное значение передается по ссылке, а значит его можно изменить изнутри метода.
class UserController // Ничего не нужно возвращать, потому что пользователь будет изменен напрямую public static void replaceName(User user, String newFirstName) user.firstName = newFirstName; > > var u = new User("Igor", "Mon"); UserController.replaceName(u, "Nina"); System.out.println(u.firstName); // => "Nina"
Сравнение
Примитивные данные сравниваются по значению. Пять всегда равно пяти, истина всегда равна истине:
var a = 5; var b = 5; a == b; // true var t1 = true; var t2 = true; t1 == t2; // true
Ссылочные сравниваются по ссылкам, а не на основе содержимого. Объекты равны только сами себе. То что хранится внутри них — не важно.
var u1 = new User("Igor", "Mon"); var u2 = new User("Igor", "Mon"); // Проверяется только ссылка, указывает она на тот же объект или нет u1 == u2; // false // Объект равен сам себе, что ожидаемо u1 == u1; // true u2 == u2; // true
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Типы данных
— Т.е. я хотел сказать «Здорова, Амиго». Хочу рассказать тебе о внутреннем устройстве переменных. Ты уже знаешь, что у каждой переменной есть область памяти, привязанная к ней, где эта переменная хранит своё значение.
— Ага. Ты рассказывал это в прошлый раз.
— Отлично. Хорошо, что ты это помнишь. Тогда продолжу.
— Все сложные типы состоят из более простых. Те, в свою очередь, из ещё более простых. Пока, наконец, дело не доходит до самых примитивных, неделимых типов. Их так и называют – примитивные типы. Например, int – это один из примитивных типов, а String – это уже сложный тип, хранящий свои данные в виде таблицы символов (где каждый символ — это примитивный тип — char).
— Очень интересно. Продолжай.
— Сложные типы образуются из простых путём группировки. Такие типы мы называем классами . Когда мы описываем в программе новый класс – это значит, что мы объявляем новый сложный составной тип , данные которого будут или другими сложными типами, или примитивными типами.
public class Person < String name; int age; >
public class Rectangle < int x, y, width, height; >
public class Cat < Person owner; Rectangle territory; int age; String name; >
— Всё пока ещё понятно, как ни странно.
— Т.к. большие (сложные) типы содержат в себе много маленьких (примитивных), то их объекты занимают много памяти. Больше, чем обычные переменные примитивных типов. Иногда намного больше. Присваивание таких переменных выполнялось очень долго и требовало копирования больших объёмов памяти. Поэтому переменные сложных типов хранят в себе не сам объект, а всего лишь ссылку на него! Т.е. четырёхбайтовый адрес. Этого хватает, чтобы можно было обращаться к данным этих объектов. Всю сложность, связанную с этим, берет на себя Java-машина.
— Ничего не понял.
— Мы уже говорили, что переменная – это как коробка. Если ты хочешь сохранить в ней число 13, то ты можешь написать его на листе и положить в коробку.
— Но представь, что тебе надо сохранить в коробку (переменную) что-нибудь побольше. Например, собаку, машину или твоего соседа Васю. Чтобы не пихать в коробку невпихиваемое, можно поступить проще: вместо собаки взять ее фото, вместо машины – ее номер, вместо Васи – его номер телефона.
— Вот мы берем лист бумаги и пишем на нем телефонный номер Васи. Это и будет аналогом ссылки на объект. Если мы достанем из коробки лист с номером Васи, отксерим его и положим в несколько коробок, то количество ссылок на Васю увеличится, но Вася как был один, так и остался. Что, в общем-то, логично.
— Особенность такого хранения данных в том, что ссылок может быть много, а объект – один.
— Очень интересно. Почти понял, кстати. Ответь только еще раз: что будет, если я одной переменной сложного типа присвою другую переменную сложного типа?
— Тогда эти две переменные будут содержать одинаковые адреса. И, значит, изменение данных, хранящихся в одной переменой сложного типа, приведёт к изменению данных, хранящихся в другой . Объект-то, на который они хранят ссылки, реально всего один. А переменных, хранящих на него ссылки, может быть очень много.
— А что хранится в переменных сложных (ссылочных/классовых) типов, пока там ещё нет ссылки на объект? Такое вообще может быть?
— Да, Амиго. Ты опередил меня своим вопросом. Такое может быть. Если в переменной ссылочного (сложного) типа ещё нет ссылки на какой-то объект, то она хранит null – специальную «пустую ссылку». На самом деле, она просто хранит адрес объекта равный 0. Но Java-машина никогда не создаёт объекты с таким адресом, и поэтому всегда знает, что если переменная-ссылка содержит 0, то никакого объекта там нет.
String s; String s = null;
Person person; person = new Person(); person = null;
Cat cat = new Cat(); cat.owner = new Person(); cat.owner.name = "God";
— Я правильно понял? Переменные делятся на два типа: примитивные и ссылочные. Примитивные типы у себя внутри хранят значение, а ссылочные – ссылку на объект. Примитивные типы – это int, char, boolean и ещё немного, а ссылочные типы – это все остальные, и образуются они с помощью классов.
— Абсолютно верно, мальчик мой.
— Раз ты все понял, вот тебе задачи на закрепление материала.
Типы данных в JavaScript
В JavaScript определены семь встроенных типов данных. Шесть примитивных типов и один тип, который представляет из себя структуру данных:
-
boolean — логический тип данных, который принимает значения true иди false
let val = false; val = true;
let val = 50; val = 3.14159; val = -200;
Этот тип представляет из себя число двойной точности, подробнее о нём можно прочитать здесь. Ноль в JavaScript имеет два представления: -0 и +0 . («0» это синоним +0). На практике это имеет малозаметный эффект. Например, выражение +0 === -0 является истинным. А также есть специальные значения, которые по сути не являются числами, но принадлежат к числовому типу данных: Infinity (бесконечность) и NaN (Not a Number — “Не Число”)
let inf = Infinity; // прямое присвоение бесконечности inf = 57 / 0; // Infinity получится при делении на ноль inf = -Infinity; // есть отрицательная бесконечность inf = 57 / -0; // -Infinity let notNumber = NaN; notNumber = "строка" * 5; // При ошибке вычисления вернёт NaN
Для получения самого большого или самого меньшего доступного значения в пределах +/-Infinity , можно использовать константы Number.MAX_VALUE или Number.MIN_VALUE . А начиная с ECMAScript 2015, вы также можете проверить, находится ли число в безопасном для целых чисел диапазоне, используя метод Number.isSafeInteger() , либо константы Number.MAX_SAFE_INTEGER и Number.MIN_SAFE_INTEGER . За пределами этого диапазона операции с целыми числами будут небезопасными, и возвращать приближённые значения. Например, подумайте над следующей задачей. Какое значение должно быть у переменной value , чтобы после выполнению кода ниже, в консоли отобразились значения, указанные в комментариях?
let value = ? console.log(i * i) // 0 console.log(i + 1) // 1 console.log(i - 1) // -1 console.log(i / i) // 1
let str = "Это строка"; console.log(str[0]); // Э - первый символ строки console.log(str.length); // 10 console.log(str[str.length - 1]); // а - последний символ строки str = 'А это "строка в двойных кавычках" внутри строки';
Также соединения строк (конкатенации) можно использовать оператор + или метод String.concat() . Также для составления строк с использованием значений переменных удобно применять шаблонизацию через обратные кавычки — `строка с переменной $
let name = "Василий"; let helloMsg = "Привет, " + name + "!"; // Привет, Василий! let newHelloMsg = name.concat(", здравствуйте. ", "Удачного дня!"); // Василий, здравствуйте. Удачного дня! //Для помещения значений переменных в шаблон используется конструкция $ helloMsg = `Привет, $name>`; // Привет, Василий!
let sym1 = Symbol(); let sym2 = Symbol("foo");
let data = null;
let data; console.log(data); // undefined
Определение типов оператором typeof
Оператор typeof возвращает строку, указывающую тип операнда. Синтаксис вызова этого оператора: typeof operand . Например:
// Числа console.log(typeof 42); // "number" console.log(typeof Infinity); // "number" console.log(typeof NaN); // "number", несмотря на то, что смысл этого значения "Not-A-Number" (не число) // Строки console.log(typeof "строка"); // "string" let name = "Василий"; console.log(typeof `Привет, $name>`); // "string" // Булевы значения console.log(typeof true); // "boolean" console.log(typeof false); // "boolean" // Символы console.log(typeof Symbol()); // "symbol" // undefined let declaredButUndefinedVariable; console.log(typeof declaredButUndefinedVariable); // "undefined"; // Объекты console.log(typeof a: 1 >); // "object" console.log(typeof [1, 2, 3]); // такая структура данных, как массив, тоже "object"
Но есть два значения, для которых оператор typeof не совсем корректно отражает их тип:
-
Значение null , для него typeof возвращает тип “object”, что является официально признанной ошибкой в языке, которая сохраняется для совместимости. На самом деле это не объект, а отдельный тип данных null .
// null console.log(typeof null); // "object"
// function console.log(typeof function() <>); // "function"
Примитивные и ссылочные типы данных
Особенность примитивных типов данных заключается в том, что они неизменяемы (иммутабельны) и передаются по значению. В отличие от объектов, которые передаются по ссылке. При этом важно понимать, что объект или примитив, это не сама переменная, а соответствующий указатель на объект или само значение примитивного типа, которое этой переменной присвоено.
Например рассмотрим следующий код:
// Примитивный тип number let num = 5; // Функция для прибавления двойки к примитиву function addTwo(num) num = num + 2; > // Вызов функции addTwo(num); console.log(num); // 5
Что происходит после вызова этой функций?
В первую функцию addTwo в качестве параметра передаётся значение переменной num из глобальной области видимости, то есть 5 . Таким образом Запись Окружения этой функции после вызова будет выглядеть так:
let num = 5; function addTwo(num) // на этапе создания контекста - , где num, это локальная переменная функции addTwo num = num + 2; // на этапе выполнения контекста, эта строка изменит запись на > addTwo(num); console.log(num); // 5
Но что же и этом произойдет со значением переменной num из глобальной области видимости? В действительности её значение останется таким же, как и было до вызова функции addTwo , так как в неё просто было скопировано значение этой переменной. И все манипуляции уже производились над другой переменной из области видимости функции addTwo . В текущей ей реализации она просто производит операцию над локальной переменной и не возвращает никакого определенного значения (а точнее она вернёт undefined ).
Если переписать эту функцию, чтобы она возвращала новое значение локальной переменной num и потом присвоить это значение обратно уже в глобальную переменную num , то только в таком случае её значение поменяется:
let num = 5; function addTwo(num) // на этапе создания контекста - num = num + 2; // на этапе выполнения контекста, эта строка изменит запись на return num; // вернуть результатом функции новое значение > num = addTwo(num); // присвоить результат функции как новое значение переменной console.log(num); // 7
Этот пример отражает то, что примитивы передаются по значению и какие-либо действия производятся над копиями этих значений. Поэтому если явно не возвращать или не присваивать новый результат в соответствующие переменные, то никаких изменений переменных не произойдет.
Теперь рассмотрим как передаются значения объектов:
// Ссылочный тип object let obj = key: 5 >; // Функция для прибавления двойки к свойству объекта function addTwoObj(obj) obj.key = obj.key + 2; > // Вызов функции addTwoObj(obj); console.log(obj.key); // 7
Почему в данном случае свойство объекта изменилось, хотя никаких дополнительных действий по возврату нового значения и его присваивания не происходило?
В этом случае важно понимать, что переменная содержит не сам объект, а грубо говоря указатель (ссылку) на то место в памяти, где этот объект хранится. Поэтому параметром в функцию будет передаваться именно это ссылка объект и Запись Окружения этой функции после вызова будет выглядеть так:
let obj = key: 5 >; function addTwoObj(obj) // на этапе создания контекста - >, здесь переменная obj опять же является уже локальной переменной функции addTwoObj obj.key = obj.key + 2; // на этапе выполнения контекста, эта строка сначала по переданной ссылке в obj найдет сам объект < key: 5 >изменит свойство key самого объекта, а не его копии. Запись Окружения станет > > addTwoObj(obj); // переменная obj хранит в себе ссылку а объект < key: 5 >, поэтому параметром передаётся именно эта ссылка console.log(obj.key); // Был изменен сам объект, который был передан по ссылке, поэтому значение его свойства будет 7
А что выведется в консоль, если изменить локальную переменную obj ?
let obj = key: 5 >; function addTwoObj(obj) obj.key = obj.key + 2; obj = num: 6 >; // присвоить другой объект obj = null; // или же вообще присвоить нулевое значение > addTwoObj(obj); console.log(obj.key);
Здесь в консоли снова выведется 7 . Так как в функции addTwoObj переменная obj является локальной и затеняет одноименную глобальную переменную, то присваивания ей в функции новых значений никак не отразится на глобальной переменной obj . Это лишь приведет к перезаписи переданной ссылки на объект < key: 5 >новыми значениями.
let obj = key: 5 >; function addTwoObj(obj) // на этапе создания контекста - >, здесь переменная obj опять же является уже локальной переменной функции addTwoObj obj.key = obj.key + 2; // на этапе выполнения контекста, эта строка сначала по переданной ссылке в obj найдет сам объект < key: 5 >изменит свойство key самого объекта, а не его копии. Запись Окружения станет > obj = num: 6 >; // после выполнения этой строки измениться лишь значение локальной переменной в Записи Окружения: > obj = null; // а здесь вообще в локальную переменную запишется значение null примитивного типа > addTwoObj(obj); // переменная obj хранит в себе ссылку а объект < key: 5 >, поэтому параметром передаётся именно эта ссылка console.log(obj.key); // Был изменен сам объект, который был передан по ссылке, поэтому значение его свойства будет 7
Обертки примитивных типов в JavaScript
В отличие от объектов, у примитивов нет своих методов, но у всех них, за исключением null и undefined , есть объектные аналоги, который оборачивает значение примитивного типа и позволяют производить над ними различные преобразования:
- String для string примитива.
- Number для number примитива.
- Boolean для boolean примитива.
- Symbol для symbol примитива.
Что происходит когда вызывается какой-либо метод у примитивного типа данных, например:
let char = "текст".charAt(1); // е
Так как у примитивного типа строки нет своих методов, то сначала создаётся его копия и неявно оборачивается в его объектный аналог с помощью конструктора new String(something) . И уже в рамках этого объекта существует набор различных встроенных методов, дин из них — charAt() , который возвращает символ строки по указанной позиции. После вызова метода возвращается его результат и эта объектная обёртка уничтожается. Поэтому сам вызов метода никак не влияет на изначальное значение примитива, а только возвращает вычисленное значение.
Явно этот код можно записать так:
let char = new String("текст").charAt(1); // "е"
И именно поэтому вызов следующих методов будет лишь возвращать новые значения и никак не повлияет на исходную переменную str :
let str = "текст"; let upper = str.toUpperCase(); let substr = str.substring(0, 3); console.log(upper); // "ТЕКСТ" console.log(substr); // "тек" console.log(str); // "текст"
У каждой такой объектной обертки есть метод valueOf() , который возвращает соответствующее значение примитивного типа. Например:
var numObj = new Number(10); console.log(typeof numObj); // object var num = numObj.valueOf(); console.log(num); // 10 console.log(typeof num); // number
Обычно не принято явно вызывать конструкторы для примитивных типов, так как они предназначены только для внутреннего использования и явное их использование без четкого понимания их поведения может приводить к различным ошибкам.
Дата изменения: February 26, 2023
Поделиться
Обнаружили ошибку или хотите добавить что-то своё в документацию? Отредактируйте эту страницу на GitHub!
Хранение по ссылке и по значению
Одно значение можно сохранить как есть, но когда их количество неизвестно, то нужен другой подход.
Время чтения: 10 мин
Открыть/закрыть навигацию по статье
- Кратко
- Примитивные типы данных
- Ссылочные типы данных
- Мутации и неизменяемость
- Аргументы функций
- Егор Огарков советует
- Каким будет значение определённого свойства объекта?
Обновлено 5 августа 2022
Кратко
Скопировать ссылку «Кратко» Скопировано
Для хранения различных значений в переменных мы используем разные типы данных. Однако хранятся эти значения по-разному. Примитивные значения (например, числа или строки) хранятся в переменной как есть, а объекты, массивы и функции — по ссылке на место в памяти.
Представим ситуацию, когда у вас в руках есть ложка из набора, и вы кладёте её в какой-то ящик. Такой простой метафорой можно описать присвоение значения в переменную, если представить ящик как переменную, а ложку как значение. Таким образом можно определить и хранение по значению. В следующий раз, когда вы захотите взять ложку, вы можете открыть тот же самый ящик и получить это значение-ложку.
Теперь представим, что у нас есть другая ложка — это специальная ложка, удобная, но при этом никто не знает, какого она размера и как её правильно хранить. Но для удобства вам хотелось так же использовать ящик. Поэтому, когда вы открыли этот же ящик, то там уже не лежит эта ложка, зато находится записка о том, где эту ложку можно найти.
В итоге, чтобы получить ложку, нужно обратиться по этому «адресу». Предположим, что теперь все такие ложки лежат в специальной «ложечной», которая может их вместить, и только оттуда их можно достать. А потому, чтобы получить ложку вам нужно обратиться по данному адресу. Аналогичным образом мы можем положить записки с тем же адресом и в другие ящики, чтобы каждый, кто обращался к ящику знал где найти ложку. Теперь ваша ложка хранится по ссылке.
В чем же фундаментальное отличие?
Отличий несколько, некоторые могут приводить к неприятным последствиями в нашем коде.
То, как будут храниться данные, жёстко связано с типом данных. Нельзя заставить значение примитивного типа храниться по ссылке, и наоборот.
Для того чтобы понять, как хранятся разные типы данных, заглянем в память компьютера.
Примитивные типы данных
Скопировать ссылку «Примитивные типы данных» Скопировано
Когда мы объявляем переменную и сохраняем в неё примитивное значение, то в память записывается какое-то количество байт, которое описывает это значение. Таким образом можно сказать, что наша переменная уже сразу содержит эти байты.
const seven = 7 // 0b0111const eight = 8 // 0b1000const seven = 7 // 0b0111 const eight = 8 // 0b1000Если присвоить какое-то значение переменной в другую, то мы просто скопируем это же количество байт в новое место.
const sevenAgain = seven // 0b0111const sevenAgain = seven // 0b0111В итоге все наши переменные можно схематически отобразить таким образом:

Когда мы сравниваем два значения, то у нас по сути произойдёт побайтовое сравнение этих величин.
console.log(seven === sevenAgain)// trueconsole.log(seven === sevenAgain) // true
console.log(seven === eight)// falseconsole.log(seven === eight) // false
Из-за того, что все примитивные значения хранятся в небольшом и фиксированном количестве байт, операции над ними выполнять несложно. Такие типы данных называют примитивными. В них входят числа ( number ), строки ( string ), булевы ( boolean ), а так же специальные значения null и undefined .
Ссылочные типы данных
Скопировать ссылку «Ссылочные типы данных» Скопировано
С объектами и другими сложными данными дела обстоят сложнее из-за того, что мы не знаем, какое количество памяти для них понадобится. Во время работы с такой структурой компьютеру необходимо следить за тем, сколько памяти уже есть, сколько понадобится, и выделять новую. Работать с такими данными сложнее. Для этого компьютер отдаёт нам ссылку на место, где данные хранятся, и самостоятельно будет работать с ними по инструкциям, которые мы ему даём. Таким образом в переменную мы получаем лишь ссылку на данные.
const myData = <>const myData = >
Обратите внимание, что направление стрелки поменялось. Так мы обозначим, что наша переменная ссылается на участок памяти.
☝️ Если сейчас присвоить значение из my Data в другую переменную, то мы скопируем ссылку, а не само значение.
const yourData = myDataconst yourData = myData
Такой тип данных называется ссылочным и в него входят объекты, массивы и функции. На самом деле и массивы и функции все они так же являются объектами, но это другая история.
Можно ли в таком случае рассчитывать, что значения будут равными? Конечно, можно! В этом случае сравниваться будут ссылки на объект, а не их содержимое. Потому, если обе переменных указываются на одно и то же, смело можно сказать, что значения равны.
const data = <>const anotherData = data console.log(data === anotherData)// trueconst data = > const anotherData = data console.log(data === anotherData) // trueИ не стоит забывать, что никакого сравнения по значениям не будет, даже если мы создадим абсолютно одинаковые объекты.
const cat = const dog = // Странно ожидать равность кошки и собаки ¯\_(ツ)_/¯ но теперь мы знаем причинуconsole.log(cat === dog)// falseconst 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)// trueconst 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.colorconst dog = name: 'Барбос', color: 'чёрный', > const puppy = . dog, // Можно выставить значение undefined, но это не удаление color: undefined, > // А это удалит поле, хоть delete считается мутированием // Но использование его на копии изменит только puppy, dog не будет изменен delete puppy.colorПопулярные в веб-разработке библиотеки React и Redux сильно завязаны на иммутабельности данных и практически построены на этом. Подробнее об этом подходе читайте в статье «Организация потоков данных».
На собеседовании
Скопировать ссылку «На собеседовании» Скопировано
- Мутации и неизменяемость