Меню Закрити

Незмінність у JavaScript

Розповідає Cristian Salcescu

Незмінна величина (immutable) — це величина, яка не може бути змінена після створення.

Уявіть, що ви чекаєте в черзі. На вході ви одержали електронний пристрій, що показує ваш порядковий номер. Перед тим, як ви майже наблизилися до вікна видачі, номер вашої черги на екрані змінився. Коли ви запитуєте, чому це сталося, стає зрозуміло, що будь-хто в цьому приміщенні може змінити ваш номер, залишаючись невідомим. Під час очікування номер черги постійно змінюється. Тож ви маєте попросити всіх у приміщенні припинити змінювати ваш номер.

Повертаючись до програмування, уявіть, що пристрій із номером — це об’єкт order із числовою (number) властивістю. Уявіть, що властивість order.number може бути змінена будь-яким фрагментом коду програми. Визначити, яка саме частина коду призвела до зміни, складно. Можливо, вам доведеться перевірити весь код, який використовує об’єкт order, і уявіть лише, як це буде непросто.

Об’єкт незмінного порядку не матиме таких проблем.

Незмінність загалом дає змогу уникнути низки несподіваних багів, які важко виявити.

Примітиви

Елементарні значення (Primitive values) є незмінними, а об’єкти-примітиви — ні.

Найкращим прикладом незмінного примітива є рядок. Будь-яка модифікація рядка призведе до появи нового рядка. Розглянемо такі приклади:

"ABCD".substring(1,3) //"BC"
"AB" + "CD"           //"ABCD"
"AB".concat("CD")     //"ABCD"

У всіх цих випадках створюються нові рядки.

Масиви

Погляньте на цей код:

const arr = [1,2,3];
doSomething(arr);
console.log(arr);
//[1,2,3] ?

Чи можемо стверджувати, що log() виведе [1, 2, 3] у консолі? Ні, не можемо. Масив є змінною структурою даних у JavaScript. Він може змінюватися у функції doSomething(). Ми повинні розуміти що робить функціяdoSomething() щоб розуміти, що саме буде виведене в консоль.

Ось приклад функції doSomething(), яка змінює масив.

function doSomething(arr){
  arr.push(4)
}

Якби масив був незмінною структурою даних, ми могли б сказати, що буде виведене в консоль без заглядання у код функції doSomething(). Таким чином можна було б заглиблюватись у меншу кількість коду.

Const

const оголошує змінну, яка не може бути перепризначена. Вона стає константою лише тоді, коли призначена величина є незмінною.

Коротше кажучи, використання const з елементарним значенням (Primitive value) оголошує константу. Використання const зі значенням об’єкта не обов’язково оголошує константу.

const book = ({
  title : "JavaScript, The good parts",
  author : "Douglas Crockford"
});
book.title = "Other title";
console.log(book);

Метод freeze()

Щоб об’єкти були незмінними, нам треба їх заморозити (freeze). Object.freeze() можна використовувати для заморожування об’єктів. Не можна додавати, видаляти чи змінювати властивості. Об’єкт стає незмінним.

const book = Object.freeze({
  title : "How JavaScript Works",
  author : "Douglas Crockford"
});
book.title = "Other title";
//Cannot assign to read only property 'title'

Object.freeze() здійснює неглибоке замороження. Вкладені об’єкти можуть бути змінені. Для глибокого замороження нам потрібно рекурсивно заморозити кожну властивість об’єкта. Ось як ми можемо застосувати deepFreeze().

function deepFreeze(object) {
   Object.keys(object).forEach(function freezeNestedObjects(name){
   const value = object[name];
    if(typeof value === "object") {
     deepFreeze(value);
    }
  });
  return Object.freeze(object);
}

deepFreeze() не робить масиви незмінними. Вона робить незмінними елементарні об’єкти.

Робота з незмінними об’єктами

Розглянемо book як незмінний об’єкт. Будь-яка зміна вимагатиме створення нового об’єкта.

Редагування властивості

const title = "JavaScript The Good Parts"
const newBook = { ...book, title };

Додавання властивості

const description = "Looking at JavaScript";
const newBook = { ...book, description };

Видалення властивості

Погляньте, як деструктуризація синтаксису використовується для створення нового об’єкта без властивості title:

const { title, ...newBook } = book;

Робота з масивом як із незмінною структурою

Структура даних масиву не є незмінною в JavaScript. Для того, щоб працювати з масивами як незмінними структурами даних, нам потрібно використовувати тільки методи чистих масивів та оператор розподілу (spread). Методи чистого масиву — це ті, що створюють новий масив, коли щось змінюється.

  • pop()push() і splice() не чисті;
  • concat()slice() – чисті;
  • sort() повинен бути чистим.

Додавання

Ось приклад додавання нового значення до незмінного масиву:

const books = [ { title : "book1" }, { title : "book2"}];
//додавання з spread
const newBooks = [ ...books, newBook ];
//додавання з concat()
const newBooks = books.concat([newBook]);

Видалення

Ось приклад видалення величини з позиції index:

const newBooks = [...books.slice(0, index), 
                  ...books.slice(index + 1)];

Бібліотека immutable.js

Immutable.js реалізує незмінні структури даних, такі як List і Map.

Попрацюймо зі структурою даних List.

Додавання

push(value) повертає новий List із новим доданим значенням.

import { List, Map } from "immutable";
const aNewBook = { title: "book3" };
const books = List([{ title: "book1" }, { title: "book2" }]);
const newBooks = books.push(aNewBook);
console.log(Array.from(books));
console.log(Array.from(newBooks));

Видалення

remove(index) повертає новий List, який виключає значення в позиції index.

const books = List([{ title: "book1" }, { title: "book2" }]);
const remainingBooks = books.remove(0);
console.log(Array.from(remainingBooks));

Тепер подивимося на Map:

const book = Map({
  title: "JavaScript The Good Parts",
  author: "Douglas Crockford"
});
const title = "How JavaScript Works"
const newBook = book.set("title", title);
console.log(newBook.toObject())

Мені подобається інтерфейс структури даних List, але я вважаю, що працювати з Map не настільки приємно, як з об’єктним літералом. Під час роботи з Map ми повинні мати доступ до всіх властивостей, як і до рядків.

Структура незмінних даних List може бути перетворена на масив за допомогою методу Array.from(), оператора поширення або методу toArray().

Структура даних Map може бути перетворена в об’єктний літерал за допомогою методу toObject(). Такі перетворення можуть бути повільними для великих масивів/об’єктів.

Під час оброблення й зберігання ми повинні враховувати незмінну структуру даних, що надається такими бібліотеками, як Immutable.js.

Найкращим варіантом буде мати незмінну структуру даних за допомогою стандартних методів JavaScript.

Об’єкти перенесення даних

Об’єкти перенесення даних — це звичайні об’єкти, що містять лише дані, які переміщуються в програмі від одного методу до іншого. Щоб уникнути колізій, вони повинні бути незмінними.

Виявлення зміни

Незмінність забезпечує ефективність у виявленні зміни об’єкта. Щоб виявити зміну об’єкта, нам просто потрібно порівняти попереднє посилання з поточним. Не потрібно перевіряти значення всіх властивостей.

Вихідні дані

Несподівані зміни можуть спричинити помилки, які важко знайти та зрозуміти. Це означатиме додаткові витрати часу для дослідження проблем, тому незмінність заощаджує час на усунення несправностей і налаштування.

Усі вихідні значення слід розглядати як незмінні значення незалежно від того, чи є вони такими, чи ні. Отже, ми уникаємо виникнення несподіваних змін будь-де в програмі.

Підсумки

Незмінність полегшує читання коду. Коли значення не можуть бути змінені, код набагато простіше зрозуміти.

Незмінність дозволяє уникнути помилок, які важко виявляти. Завдяки незмінності ми звужуємо простір для несподіванок.

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

0

Повідомити про помилку

Текст, який буде надіслано нашим редакторам: