DevGang
Авторизоваться

Неизменяемость в JavaScript – объяснение на примерах

Мы часто слышим термины: неизменяемый и неизменяемость. Но что они означают, и почему нас, разработчиков, это должно волновать?

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

Но большинство программ требуют создания, обновления и удаления данных. Так зачем же нам вообще работать с данными, которые невозможно изменить?

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

Вы также можете посмотреть соответствующее видео здесь:

А вот пример JavaScript (который вы также можете просмотреть на Stackblitz):

// Import stylesheets
import './style.css';

// Write JavaScript code!
const appDiv = document.getElementById('app');
appDiv.innerHTML = `<h1>Open the console to see results</h1>`;

class Person {
  //_name = "Nee";
  //_name = ["Nee", "Ra"];
  _name = { first: "Nee", middle: "L" };
  
  get name() {
    return this._name;
  }
  
  set name(value) {
    console.log('In setter', value);
    this._name = value;
  }
}

let p = new Person();
//p.name = "Ra";                        // Setter executes
//p.name.push("Lee");                   // Setter doesn't execute
//p.name = [...p.name, "Lee"];          // Setter executes
//p.name.middle = "Lee";                // Setter doesn't execute
p.name = { ...p.name, middle: "Lee" };  // Setter executes

Начнем с примитивов.

Примитивы в JavaScript: естественно неизменяемые

В JavaScript примитивы, такие как строки и числа, по умолчанию являются неизменяемыми. Это означает, что после создания примитивного значения его нельзя изменить. Подождите, вы можете подумать: я все время меняю значения примитивных переменных!

Ну, может показаться, что вы меняете значение. Но на самом деле это не так. Вот пример.

let greet = "Hello";
greet += ", World";  
console.log(greet);

Первая строка этого кода создает строку Hello и присваивает ее переменной greet. Вторая строка добавляет , World к этой строке. Похоже, мы меняем greet строку. Но JavaScript не меняет строку. Скорее, он создает новую строку.

Давайте посмотрим на иллюстрацию. Здесь у нас есть greet переменная, присвоенная строке Hello.

Рисунок 1. Код создает строку&nbsp;Hello и присваивает ее переменной&nbsp;greet.
Рисунок 1. Код создает строку Hello и присваивает ее переменной greet.

Когда код добавляет текст, JavaScript создает новую строку. Затем он присваивает greet переменную этой новой строке. Исходная Hello строка не изменяется.

Рисунок 2. Добавление текста создает новую строку и присваивает ее переменной&nbsp;greet.
Рисунок 2. Добавление текста создает новую строку и присваивает ее переменной greet.

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

Как насчет массивов?

Массивы JavaScript изменяемы

В JavaScript массивы по умолчанию изменяемы. Это означает, что массив можно изменить после его создания. Мы можем модифицировать его «на месте», добавляя, удаляя или изменяя элементы.

Давайте посмотрим на пример.

let ages = [42, 22, 35];
ages.push(8);  
console.log(ages);

Первая строка кода определяет массив и присваивает этому массиву переменную. Но в JavaScript переменная не хранит массив. Он хранит адрес памяти, в которой находится массив, как показано на рисунке 3:

Рисунок 3. Переменная не хранит массив — она хранит адрес памяти массива.
Рисунок 3. Переменная не хранит массив — она хранит адрес памяти массива.

Во второй строке кода предыдущего примера мы используем push метод для изменения исходного массива. В этом случае мы добавляем 8 в конец массива. Это показано на рисунке 4:

Рисунок 4. Массив JavaScript изменяется «на месте».
Рисунок 4. Массив JavaScript изменяется «на месте».

Обратите внимание, что адрес памяти массива не меняется, но меняется сам массив. Таким образом, значение массива является изменчивым.

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

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

Как мы видели в этом примере, наш массив изменился... но наша ages переменная на самом деле не изменилась, поскольку она ссылается на адрес памяти. Таким образом, установщик, обнаружение изменений или управление состоянием могут не знать, что массив был изменен.

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

Как реализовать неизменность с помощью массивов

Давайте посмотрим на альтернативный пример:

let ages = [42, 22, 35];
ages = [...ages, 8];  
console.log(ages);

В этом коде мы начинаем с того же массива. Но когда мы добавляем элемент, мы используем оператор spread JavaScript. Оператор распространения создает копию существующего массива по новому адресу, «расширяя» существующий массив.

Затем мы добавляем новый элемент в эту копию. Также переназначаем agesпеременную на адрес нового массива (рисунок 5).

Рисунок 5. Используя оператор расширения, мы создаем новый массив по новому адресу и присваиваем его переменной&nbsp;ages.
Рисунок 5. Используя оператор расширения, мы создаем новый массив по новому адресу и присваиваем его переменной ages.

Обратите внимание, что исходный массив не изменяется. Используя оператор распространения, мы достигаем неизменности.

Помимо оператора расширения, многие методы массива также создают новый массив и, следовательно, рассматривают его как неизменяемый. Другие методы массива изменяют массив на месте и поэтому являются изменяемыми. Вот некоторые примеры.

  • Map создает новый массив из существующего массива, сопоставляя каждый элемент с помощью предоставляемой нами функции. Он оставляет исходный массив неизменным. Таким образом, он поддерживает неизменность.
ages.map(x => x + 1);
  • Push модифицирует исходный массив на месте, изменяя массив.
ages.push(8);
  • Filter создает новый массив с элементами, соответствующими определенным критериям. Он оставляет исходный массив неизменным.
ages.filter(x => x > 21);
  • Sort сортирует элементы массива на месте, тем самым изменяя массив.
ages.sort();
  • Slice создает новый массив из части существующего массива. Здесь мы копируем исходные элементы массива, начиная с индекса 1–3, в новый массив.
ages.slice(1, 3);
  • Splice изменяет содержимое массива на месте, добавляя, удаляя или заменяя существующие элементы. В этом примере код начинает замену элементов с индекса 2, заменяет только 1 элемент и заменяет элемент на «18».
ages.splice(2, 1, 18);

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

А как насчет объектов?

Изменяемая природа объектов JavaScript

Объекты в JavaScript также изменяемы по умолчанию. Мы можем добавлять или удалять свойства и изменять их значения «на месте» после создания объекта.

let p = {name:"Nee", age: 30};
p.age = 31;
console.log(p);

В первой строке этого примера кода объявляется объект person со свойствами имени и возраста. Когда этому объекту присваивается переменная, в ней хранится не сам объект, а адрес памяти, в которой находится объект.

Рисунок 6. Переменная не хранит объект. Скорее, он хранит адрес памяти объекта.
Рисунок 6. Переменная не хранит объект. Скорее, он хранит адрес памяти объекта.

Вторая строка кода в предыдущем примере изменяет значение свойства объекта, меняя возраст. Эта модификация напрямую изменяет исходный объект person.

Рисунок 7. Объект JavaScript изменяется «на месте».
Рисунок 7. Объект JavaScript изменяется «на месте».

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

Изменяемость обеспечивает гибкость, но может привести к сложным ошибкам, если ею не управлять тщательно.

И, как и в случае с массивом, изменчивость имеет проблемы. Предположим, у вас есть код в установщике, который должен выполняться при изменении объекта. Или вы работаете с инфраструктурой, такой как Angular, которая обеспечивает обнаружение изменений. Или вы используете библиотеку состояний, например Redux, которая требует неизменяемости.

В этом примере наш объект изменился, но наша p переменная фактически не изменилась, поскольку она ссылается на адрес памяти. Таким образом, установщик, обнаружение изменений или управление состоянием могут не увидеть, что объект был изменен.

Зачастую лучше обрабатывать объекты неизменяемым образом. JavaScript предоставляет функции, помогающие работать с неизменяемыми объектами.

Неизменяемость с объектами

Вот альтернативный пример:

let p = {name:"Nee", age: 30};
p = {...p, age: 31};
console.log(p);

Начнем с того же объекта. Но вместо непосредственного изменения значения свойства объекта мы снова используем оператор распространения. Оператор распространения создает копию объекта, распространяя его в новый объект по новому адресу. Мы обновляем свойство в этом новом объекте. Затем мы переназначаем p переменную на адрес нового объекта.

Рисунок 8. Используя оператор распространения, мы создаем новый объект по новому адресу и присваиваем его переменной&nbsp;p.
Рисунок 8. Используя оператор распространения, мы создаем новый объект по новому адресу и присваиваем его переменной p.

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

Итак, мы увидели, что примитивы по умолчанию неизменяемы. И что массивы и объекты изменяемы, но мы можем работать с ними неизменяемым образом.

Хороший! Но почему нас это волнует?

Почему важна неизменность?

Есть несколько причин, по которым неизменяемость важна для нашего повседневного кодирования.

  • После установки неизменяемого значения оно не изменяется. Скорее создается новая ценность. Это делает значение предсказуемым и согласованным во всем коде. Таким образом, это помогает управлять состоянием во всем приложении. Плюс неизменность — ключевой принцип в средах управления состоянием, таких как Redux.
  • Код становится проще и менее подвержен ошибкам, если структуры данных не изменяются неожиданно. Это также упрощает отладку и обслуживание.
  • Использование неизменности соответствует принципам функционального программирования, что приводит к меньшему количеству побочных эффектов и более предсказуемому коду.

Подведение итогов

Неизменяемость — фундаментальная концепция программирования. Неизменяемое значение — это значение, которое нельзя изменить после его создания.

Эта концепция важна для функционального программирования и управления состоянием. Это ценная концепция, особенно при работе с параллелизмом и большими и сложными базами кода.

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

Источник:

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу