Что значит мутировать массив
Разбираем на примерах: как избежать мутаций в JavaScript
furry.cat
Мутация в JavaScript – это изменение объекта или массива без создания новой переменной и переприсваивания значения. Например, вот так:
Проблемы с мутациями
Казалось бы – ничего страшного. Но такие маленькие изменения могут приводить к большим проблемам.
Подобные ошибки трудно заметить, ведь операции выполняются нормально – только с результатом что-то не так. Функция рассчитывает на один аргумент, а получает «мутанта» – результат работы другой функции.
Решением являются иммутабельные (неизменяемые) структуры данных. Эта концепция предусматривает создание нового объекта для каждого обновления.
К сожалению, иммутабельность из коробки в JavaScript не поддерживается. Существующие решения – это более или менее кривые костыли. Но если вы будете максимально избегать мутаций в коде, код станет понятнее и надежнее.
Избегайте мутирующих операций
Проблема
Распространенная мутация в JavaScript – изменение объекта:
В этом примере мы создаем объект с тремя полями, поле settings опционально. Для добавления объекта мы мутируем исходный объект example – добавляем новое свойство. Чтобы понять, как выглядит в итоге объект example со всеми возможными вариациями, нужно просмотреть всю функцию. Было бы удобнее видеть его целиком в одном месте.
Решение
Чтобы очистить код, вынесем вычисление settings в отдельную функцию:
Теперь проще понять и что делает фрагмент, и форму возвращаемого объекта. Благодаря рефакторингу мы избавились от мутаций и уменьшили вложенность.
Будьте осторожны с мутирующими методами массивов
Далеко не все методы в JavaScript возвращают новый массив или объект. Многие мутируют оригинальное значение прямо на месте. Например, push() – один из самых часто используемых.
Проблема
Посмотрим на этот код:
Сама по себе мутация – не такая уж большая проблема. Но где мутации, там и другие подводные камни. Проблема этого фрагмента – императивное построение массива и различные способы обработки постоянных и опциональных строк.
Решение
Одна из полезных техник рефакторинга – замена императивного кода, полного циклов и условий, на декларативный. Давайте объединим все возможные ряды в единый декларативный массив:
Код стал читаемее и удобнее для поддержки:
Проблема
Решение
Код выполняет две задачи внутри одного цикла:
Для улучшения читаемости следует разделить операции:
Другие мутирующие методы массивов, которые следует использовать с осторожностью:
Избегайте мутаций аргументов функции
Так как объекты и массивы в JavaScript передаются по ссылке, их изменение внутри функции приводит к неожиданным эффектам в глобальной области видимости.
Проблема
Подобные мутации могут быть и преднамеренными, и случайными. И то, и то приводит к проблемам:
Этот код конвертирует набор числовых переменных в массив messageProps со следующей структурой:
Проблема в том, что функция addIfGreateThanZero вызывает мутации массива, который мы ей передаем. Это изменение преднамеренное, оно необходимо для работы функции. Однако это не самое лучшее решение – можно создать более понятный и удобный интерфейс.
Решение
Давайте перепишем функцию, чтобы она возвращала новый массив:
Но от этой функции можно полностью отказаться:
Можно еще немного упростить:
Но это приводит к менее очевидному интерфейсу и не позволяет использовать автокомплит в редакторе кода.
Кроме того, создается ложное впечатление, что функция принимает любое количество аргументов и в любом порядке. Но это не так.
Однако код с reduce выглядит менее очевидным и труднее читается, поэтому стоило бы остановиться на предыдущем шаге рефакторинга.
Похоже, что единственная веская причина для мутации входящих параметров внутри функции – это оптимизация производительности. Если вы работаете с огромным объемом данных, то создание нового объекта/массива каждый раз – довольно затратная операция. Но как и с любой другой оптимизацией – не спешите, убедитесь, что проблема действительно существует. Не жертвуйте чистотой и ясностью кода.
Если вам нужны мутации, сделайте их явными
Проблема
Решение
Лучше сделать мутацию явной:
Другой вариант – обернуть встроенные мутирующие операции кастомной функцией и использовать ее:
Также вы можете применять сторонние библиотеки, например, функцию sortBy библиотеки Lodash:
Обновление объектов
В современном JavaScript появились новые возможности, упрощающие реализацию иммутабельности – спасибо spread-синтаксису. До его появления нам приходилось писать что-то такое:
Теперь можно писать проще:
Суть та же, но гораздо менее многословно и без странного поведения.
А до введения стандарта ECMAScript 2015, который подарил нам Object.assign, избежать мутаций было и вовсе почти невозможно.
В документации библиотеки Redux есть замечательная страница Immutable Update Patterns, которая описывает концепцию обновления массивов и объектов без мутаций. Эта информация полезна, даже если вы не используете Redux.
Подводные камни методов обновления
Как бы ни был хорош spread-синтаксис, он тоже быстро становится громоздким:
Чтобы изменить глубоко вложенных полей, приходится разворачивать каждый уровень объекта, иначе мы потеряем данные:
В этом фрагменте кода мы сохраняем только первый уровень свойств исходного объекта, а свойства lunch и drinks полностью переписываются.
Если вам приходится часто обновлять какую-то структуру данных, лучше сохранять для нее минимальный уровень вложенности.
Пока мы ждем появления в JavaScript иммутабельности из коробки, можно упростить себе жизнь двумя простыми способами:
Отслеживание мутаций
Линтинг
ReadOnly
Другой способ – пометить все объекты и массивы как доступные только для чтения, если вы используете TypeScript или Flow.
Вот пример использования модификатора readonly в TypeScript:
Использование служебного типа Readonly :
То же самое для массивов:
В плагине eslint-plugin-functional есть правило, которое требует везде добавлять read-only типы. Его использование удобнее, чем их ручная расстановка. К сожалению, поддерживаются только модификаторы.
Заморозка
Упрощение изменений
Для достижения наилучшего результата следует сочетать технику предотвращения мутаций с упрощением обновления объектов.
Самый популярный инструмент для этого – библиотека Immutable.js:
Используйте ее, если вас не раздражает необходимость изучить новый API, а также постоянно преобразовывать обычные массивы и объекты в объекты Immutable.js и обратно.
Другой вариант – библиотека Immer. Она позволяет работать с объектом привычными методами, но перехватывает все операции и вместо мутации создает новый объект.
Immer также замораживает полученный объект в процессе выполнения.
Иногда в мутациях нет ничего плохого
В некоторых (редких) случаях императивный код с мутациями не так уж и плох, и переписывание в декларативном стиле не сделает его лучше. Рассмотрим пример:
Здесь мы создаем массив дат в заданном диапазоне. У вас есть идеи, как можно переписать этот код без императивного цикла, переприсваивания и мутаций?
В целом этот код имеет право на существование:
Если вы используете мутации, постарайтесь изолировать их в маленькие чистые функции с понятными именами.
Руководство к действию
А как вы боретесь с мутациями в JavaScript коде?
Как я работаю с массивами в JavaScript
17 различных вариантов, Карл! 😱.
В этой статье я хочу задокументировать, как я выбираю методы при работе с массивом, расскажу, что и когда я использую. Это должно помочь вам понять подход выбора метода при работе.
Мутация
Никогда не мутируйте массивы. Это может сломать ваш код, а вы этого и не заметите. И подобные ошибки, к слову, трудно отловить.
Добавление элементов в массивы
Тут мы рассмотрим, как в JavaScript добавить элемент в массив. Сделать мы это можем тремя способами:
Добавление элементов в начало массива
Добавление элементов в конец массива
Добавление элементов в середину массива
Я предпочитаю splice при добавлении элементов в середину массива. Я делаю это потому, что использование одного только slice кажется более неуклюжим.
splice намного легче читать, по сравнению с только slice альтернативой.
Удаление элементов из массива
Здесь рассмотрим подходы в JavaScript для удаления элемента из массива. Мы можем сделать это тремя способами:
Удаление элементов с самого начала массива
Удаление элементов с конца массива
Удаление элементов с середины массива
Цикл по массиву
Когда я перебираю элеметны массива в цикле, я предпочитаю использовать map и filter везде, где это возможно. Отлично, если их хватает для моих задач!
Если вы работаете с ассоциативными массивами, или объектами, которые нужно обработать в цикле, то ранее мы писали статью на эту тему.
Асинхронные циклы
Вот и всё! Надеюсь, эта статья вам помогла! Надеюсь, эта статья здорово помогла вам при работе с массивами в JavaScript.
Subscribe to Блог php программиста: статьи по PHP, JavaScript, MySql
Get the latest posts delivered right to your inbox
Методы массивов в JavaScript: С мутацией или без
JavaScript предоставляет несколько способов для добавления, удаления и замены элементов в массиве. Но некоторые из них используют мутацию, то есть видоизменяют изначальный массив, а некоторые — нет, они просто создают новый массив.
Несмотря на то, что я здесь не привожу полный список, ниже приводятся стратегии для выполнения любой основной манипуляции над массивом.
ВНИМАНИЕ: Пока будете читать эту статью, обращайте особое внимание разнице между:
I. Добавление: С мутацией
Это код иллюстрирует, что:
II. Добавление: Без мутации
Есть два способа, чтобы добавить новые элементы в массив без мутации изначального массива.
В примере выше, оператор расширения будет копировать оригинальный массив, доставать все его элементы и размещать их в новом контексте.
Далее происходит почти то же самое, но новый элемент ‘z’ добавляется перед другими элементами.
III. Удаление: С мутацией
Этот код иллюстрирует, что:
array.pop() и array.shift() возвращают элемент, который был удален. Это означает, что вы можете «поймать» удаленный элемент и поместить в переменную.
Также есть array.splice() для удаления элементов массива.
mutatingRemove.splice(0, 2) на примере выше принимает два параметра (он может принимать больше двух, подробнее ниже).
В примере выше, два элемента удаляются из массива mutatingRemove (второй аргумент), начиная с 0 индекса (первый аргумент).
IV. Удаление: Без мутации
Метод JavaScript-а array.filter() создает новый массив из первоначального массива, но новый содержит только те элементы, которые соотвествуют заданному критерию.
Некоторые особенности стрелочных функций:
Для однострочных стрелочных функций ключевое слово return подразумевается по умолчанию, так что вам не нужно писать его.
Однако для многострочных стрелочных функций нужно явно указывать возвращаемое значение.
array.slice() принимает два аргумента.
На следующей строке const arr3 = arr1.slice(2) показан полезный трюк. Если второй параметр метода array.slice() не задан, то метод берет копию с начального индекса до конца массива.
V. Замена: С мутацией
Для этого, нужно использовать хотя бы 3 аргумента:
mutatingReplace.splice(2, 1, 30) заменяет ‘c’ на 30.
mutatingReplace.splice(2, 1, 30, 31) удаляет ‘c’ и добавляет 30 и 31.
VI. Замена: Без мутации
Преобразование данных при помощи array.map()
array.map() является полезным методом, который может быть использован для преобразования данных, не ставя под угрозу целостность изначальных данных.
Если вам понравилась эта статья, поделитесь ею с другими.
Про мутацию данных в JavaScript
В JavaScript существует похожая проблема с мутацией данныз. Если ваш код мутируемый, вы можете изменить какую-то одну часть (и сломать) что-нибудь в другом месте, не зная об этом.
Объекты являются мутируемымы в JavaScript
В JavaScript можно динамически добавлять свойства к объекту. Когда вы делаете это после инстанцирования, объект изменяется навсегда. Он мутирует, например, как член Людей Икс мутирует, когда они получают силы.
Мутация в JavaScript вполне нормальна. Вы используете её постоянно.
Вот ситуация, когда мутация становится страшной.
Такое странное поведение происходит из-за того, что объекты в JavaScript передаются по ссылке.
Объекты в JavaScript передаются по ссылке
Примитивные типы в JavaScript являются иммутабельными
const не решает проблему мутабельности
Объявление переменной с помощью const не делает её иммутабельной, оно просто не позволяет присвоить ей другое значение.
Предотвращение мутации объектов
Object.assign
Object.assign позволяет объединить два (или более) объекта в один. Он имеет следующий синтаксис:
При обнаружении двух конфликтующих свойств, свойство в более позднем объекте перезаписывает свойство в более раннем объекте (в Object.assign аргументах).
Но всегда помните! При объединении двух объектов с Object.assign первый объект мутируется. Другие объекты остаются неизменными.
Решение проблемы мутации Object.assign
С этого момента вы можете мутировать ваш новый объект, как вам угодно. Это не влияет ни на один из ваших предыдущих объектов.
К слову, это очень распространённый приём создания нового иммутабельного объекта. Подобный приём очень часто применяется в React.
Но Object.assign копирует ссылки на объекты
Рассмотрим это утверждение на примере.
Предположим, вы покупаете новую звуковую систему. Система позволяет задать состояние питания, задать громкость, размер басов и прочие опции.
Некоторые из ваших друзей любят громкую музыку, поэтому вы решили создать предустановку, которая гарантированно разбудит ваших соседей, когда они уснут.
Потом приглашаешь друзей на вечеринку. Чтобы сохранить существующие заготовки, вы пытаетесь объединить громкую заготовку с заготовкой по умолчанию.
Так как Object.assign выполняет высокоуровневое слияние, вам нужно использовать другой метод для слияния объектов, которые содержат вложенные свойства (т.е. объекты внутри объектов).
Assignment
Assignment это небольшая библиотека, которая является отличным материалом для более глубокого изучения JavaScript.
Assignment копирует значения всех вложенных объектов, что исключает возможность к мутации данных.
Нужно ли всегда использовать Assignment вместо Object.assign?
Как убедиться, что объекты не были мутированы
Хотя упомянутые мною методы могут помочь вам предотвратить мутацию объектов, они не гарантируют, что объекты не мутируют. Если вы допустили ошибку и использовали Object.assign для вложенного объекта, то в дальнейшем у вас будут серьезные неприятности.
Чтобы обезопасить себя, вы можете гарантировать, что объекты вообще не мутируют. Для этого вы можете использовать библиотеки типа ImmutableJS. Эта библиотека выбрасывает ошибку всякий раз, когда вы пытаетесь мутировать какой-то объект.
Object.freeze и deep-freeze
Object.freeze предотвращает непосредственное изменение свойств объекта.
Для предотвращения глубокой мутации можно использовать библиотеку deep-freeze, которая рекурсивно вызывает Object.freeze каждому из объектов.
Не путайте переназначение с мутацией
Когда вы мутируете объект, меняется сам объект, но ссылка на объект остается неизменной.
Резюме
В этой статье мы разобрали, как работает Object.assign и Object.freeze в JavaScript, а так же, как бороться с мутабельностью данных в плоских и рекурсивных свойствах.
Обратите внимание, что Object.assign и Object.freeze могут предотвращать первоуровневые свойства от мутации. Если вам нужно предотвратить мутацию объектов нескольких уровне вложенности, вам понадобятся такие библиотеки, как assignment и deep-freeze.
Subscribe to Блог php программиста: статьи по PHP, JavaScript, MySql
Get the latest posts delivered right to your inbox
JavaScript и ужасы мутаций
Мутация — это изменение. Изменение формы или изменение сути. То, что подвержено мутациям, может меняться. Для того чтобы лучше осознать природу мутации — подумайте о героях фильма «Люди Икс». Они могли внезапно получать потрясающие возможности. Однако проблема заключается в том, что неизвестно, когда именно эти возможности проявятся. Представьте себе, что ваш товарищ ни с того ни с сего посинел и оброс шерстью. Страшновато, правда? В JavaScript существуют те же проблемы. Если ваш код подвержен мутациям, это значит, что вы можете, совершенно неожиданно, что-то изменить и поломать.
Объекты в JavaScript и мутация
В JavaScript-объекты можно добавлять свойства. Когда это делают после создания экземпляра объекта, объект необратимо изменяется. Он мутирует, как один из персонажей «Людей Икс».
Мутации — вполне обычное явление в JavaScript. Столкнуться с ними можно буквально всегда и везде.
Об опасности мутаций
Вышеприведённый пример иллюстрирует опасность мутаций. Она сводится к тому, что когда вы меняете что-то в коде, то нечто, находящееся где-то в другом месте, тоже может поменяться, причём так, что вы об этом и знать не будете. В результате — ошибки, которые сложно находить и исправлять.
Все эти странности являются следствием того, что объекты в JavaScript передаются по ссылке.
Объекты в JavaScript и ссылки на них
К сожалению, в ситуациях, схожих с описанной, обычно не нужно, чтобы то, что записано в одну переменную, менялось при воздействии на другую, так как это приводит к неправильному поведению кода, которое проявляется тогда, когда этого ждут меньше всего. Итак, как же предотвратить мутации объектов? Прежде чем найти ответ на этот вопрос, хорошо бы сначала узнать, что в JS является иммутабельным, то есть — неизменным.
Иммутабельные примитивы
Ключевое слово const и иммутабельность
Использование ключевого слова const не делает то, что записано в константу, иммутабельным. Оно лишь не даёт назначить константе новое значение.
Предотвращение мутаций объектов
▍Метод Object.assign
Конструкция Object.assign позволяет комбинировать два объекта (или большее число объектов), получая на выходе один новый объект. Пользоваться ей можно так:
▍Решение проблемы мутации при использовании Object.assign
В качестве первого объекта Object.assign можно передать новый объект для того, чтобы предотвратить мутацию существующих объектов. Однако, первый объект (пустой) всё ещё подвергается изменениям, но тут нет ничего страшного, так как мутация больше ничего важного не затрагивает.
Новый объект после выполнения этой операции можно менять как угодно. Это не затронет предыдущие объекты.
▍Object.assign и ссылки на объекты-свойства
Ещё одна проблема с Object.assign заключается в том, что он выполняет поверхностное слияние объектов (shallow merge) — он копирует свойства напрямую из одного объекта в другой. При этом он копирует и ссылки на объекты, являющиеся свойствами обрабатываемых объектов.
Рассмотрим это на примере.
Предположим, вы купили новую звуковую систему. Вы можете управлять её питанием, устанавливать громкость, уровень баса и другие параметры. Вот как выглядит стандартная конфигурация системы.
Некоторые из ваших друзей любят громкую музыку, поэтому вы решили сделать предустановку, которая гарантированно поставит на уши весь дом.
▍Библиотека assignment
Библиотека выполняет копирование значений всех объектов, вложенных в другие объекты, в новый объект, что предохраняет существующие объекты от мутации.
Библиотека assignment — это лишь один из многих инструментов, позволяющих выполнять глубокое слияние объектов. Другие библиотеки, включая lodash.assign и merge-options, тоже могут вам в этом помочь. Можете спокойно выбрать ту, что вам больше понравится.
Всегда ли необходимо использовать глубокое слияние вместо Object.assign?
Обеспечение иммутабельности объектов
Хотя те методы, о которых мы говорили выше, могут помочь защитить объекты от мутаций, они не гарантируют иммутабельность созданных с их помощью объектов. Если вы сделаете ошибку и используете Object.assign при работе с объектом, имеющим вложенные свойства-объекты, позже у вас могут быть серьёзные неприятности.
Для того чтобы от этого защититься, стоит обеспечить гарантию того, что объект не будет мутировать вообще. Для этого можно использовать библиотеку наподобие ImmutableJS. Эта библиотека выдаёт ошибку при попытке изменения обработанного с её помощью объекта.
Метод Object.freeze и библиотека deep-freeze
Метод Object.freeze защищает собственные свойства объекта от изменений.
Для предотвращения мутации объектов-свойств, можно использовать библиотеку deep-freeze, которая рекурсивно вызывает Object.freeze для всех свойств «замораживаемого» объекта, являющихся объектами.
О перезаписи значений и мутации
При мутации же меняется сам объект. Ссылка на объект, записанная в переменную или константу, остаётся той же самой.
Итоги
Мутации опасны потому, что они могут нарушить работу кода, причём, сделать это совершенно незаметно и непредсказуемо. Если даже вы подозреваете, что причина проблемы в мутации, поиск проблемного места — та ещё задачка. Поэтому лучший способ защитить код от неприятных неожиданностей — это обеспечить, с момента создания объектов, их защиту от мутаций.
Обратите внимание на то, что методы Object.assign и Object.freeze могут защитить от изменений только собственные свойства объектов. Если нужно защитить от мутаций и свойства, которые сами являются объектами, понадобятся библиотеки вроде assignment или deep-freeze.
Уважаемые читатели! Сталкивались ли вы с неожиданными ошибками в JS-приложениях, вызванными мутациями объектов?