Неизменяемость в Javascript: как и почему?

Неизменяемость - ключевой принцип функционального программирования. В этой статье мы рассмотрим некоторые из лучших практик, используемых для сохранения неизменности структур данных в JavaScript, и объясним, почему это полезно. Приступим.

Неизменяемость с использованием Object.freeze.

Для несложных структур данных неизменяемость может быть достигнута «замораживанием» ваших объектов. Метод Object.freeze дает возможность заблокировать объект от дальнейших изменений, но только для всех свойств объекта, которые в javascript являются неизменяемыми типами данных.

В случае кода, подобного приведенному ниже примеру, этот подход работает нормально.

const objFrozen = Object.freeze({ a: "1", b: false, c: 2});
objFrozen.a = "3";
objFrozen.b = true;
objFrozen.c = 4;
console.log(objFrozen); // { a: "1", b: false, c: 2 }

Что, если свойства вашего объекта являются изменяемыми типами данных?

Для случаев, подобных приведенному ниже примеру, Object.freeze недостаточно.

const objFrozen = Object.freeze({ a: "1", b: false, c: [1,2,3]});
objFrozen.a = "3";
objFrozen.b = true;
objFrozen.c.push(4);
console.log(objFrozen); // { a: "1", b: false, c: [1,2,3,4]} }

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

Какие типы данных в Javascript в таком случае неизменяемы?

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

Код вроде:

let sourceStr = "hello";
let resultStr = sourceStr.slice(1, 3);

не изменит строку в sourceStr, но новая строка будет назначена для resultStr, сохраняя неизменность sourceStr. Та же концепция действительна для чисел и логических значений.

А что насчет предметов?

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

В примере:

let sourceArray = [1,2,3];
let resultArray = sourceArray;
resultArray.push(4);

resultArray указывает точно на тот же массив, на который указывает sourceArray, это означает, что если вы сравните две переменные sourceArray === resultArray вывод будет «true».

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

Мутация - побочные эффекты

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

Мы, javascript-функция arrayUtils, которой в качестве аргумента был задан массив, возвращает объект с двумя служебными функциями: sortInput и getInput:

let sourceArray=[3,2,1];
const arrayUtils = input => {
 const sortInput = () => input.sort();
 const getInput = () => input;
 return { sortInput, getInput }
}
const util = arrayUtils(sourceArray);
console.log(util.sortInput()); // [1, 2, 3]
console.log(util.getInput()); // [1, 2, 3]

Как видите, вывод на консоль такой же. Функция util. sortInput изменила sourceArray, что привело к неожиданному результату, выводимому на консоль с помощью util. getInput. В реальном сценарии использования sourceArray может использоваться в различных компонентах внутри вашего приложения, факт мутации util. sortInput может вызвать неожиданное поведение во всех функциях вашего приложения, которые будут иметь дело с sourceArray. Такие побочные эффекты действительно сложно обнаружить в больших приложениях.

Что же нам делать, чтобы избежать этих побочных эффектов?

В JavaScript нет неизменяемых структур из коробки, сторонние библиотеки, такие как immutable.js, написанные разработчиками Facebook, являются хорошим решением для сохранения структур данных вашего приложения. неизменный.

Если вы не хотите использовать какую-либо стороннюю библиотеку javascript, вам придется сделать это самостоятельно. Посмотрим как.

Операции с массивами.

Клонирование массива примитивных типов данных.

const sourceArray = [1,2,3];
const clonedArray = [...sourceArray];
// or you can do
const clonedArray = sourceArray.slice(0);

Клонирование массива объектов, свойства которых являются примитивными типами данных.

const sourceArray = [{ a: 1}, { b: 2 }, { c: 3}];
const clonedArray = sourceArray.map(item => ({...item}));

Добавление нового элемента в массив.

const sourceArray = [1,2,3];
const newArray = [...sourceArray, 4];
const sourceArray = [{ a: 1}, { b: 2 }, { c: 3}];
const newArray = [...sourceArray, { d: 4}];

Удаление элемента из массива.

const itemToRemove = 3;
const sourceArray = [1,2,3];
const newArray = sourceArray.filter(item => item !== itemToRemove);

Замена элемента в массиве.

const itemToAdd = { id: 2, a: 4 };
const sourceArray = [{id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3}];
// replacing without caring about position
const newArray = [...sourceArray.filter(item => item.id !== itemToAdd.id), itemToAdd];
// replacing caring about position
const indexOldElement = sourceArray.findIndex(({ id }) => id == itemToAdd.id);
const newArray = Object.assign([...sourceArray], {[indexOldElement]: itemToAdd});
// or you can do
const newArray = [...sourceArray.slice(0, indexOldElement), itemToAdd, ...sourceArray.slice(indexOldElement + 1)]

Операции с объектами.

Добавление новой опоры.

const sourceObj = { a: 1, b: 2};
const newProp = { c: 3 };
const newObj = { ...sourceObj, ...newProp};
// or you can do
const c = 3;
const newObj = { ...sourceObj, c};
// newObj = { a: 1, b: 2, c: 3};

Удаление опоры.

const sourceObj = { a: 1, b: 2, c: 3};
const { b, ...newObj } = sourceObj;
// console.log(newObj) => { a: 1, c: 3};

Обновите вложенный объект, свойства которого являются примитивами.

const sourceObj = { a: 1, b: 2, c: { d: 3, e :4 } };
const c = { ...sourceObj.c, f: 5 }
const newObj = { ...sourceObj, c };

Обновите вложенный объект, свойства которого не являются примитивами.

const sourceObj = { a: 1, b: 2, c: { d: [1, 2, 3 ], e :4 } };
const d = [ ...sourceObj.c.d, 4 ];
const c = { ...sourceObj.c, d }
const newObj = { ...sourceObj, c };

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

Что сказать о производительности?

Создание нового объекта требует больших затрат времени и памяти, но во многих случаях этих недостатков меньше, чем преимуществ.

Такие преимущества, как возможность быстрого сравнения двух неизменяемых объектов, полагаясь только на проверку идентичности / строгого равенства оператора oldObject === newObject или , уменьшающие вероятность При устранении неприятных ошибок, как описано в примере выше, вам следует подумать о сохранении определенного качества и читабельности кода, прежде чем начинать кодирование.

Заключение

Я надеюсь, что вам понравилось читать эту статью, и она дала вам больше информации о том, что означает неизменяемость, и о том, как вы можете реализовать ее без использования сторонней библиотеки javascript.