Неизменяемость в 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.