Автор: Кристиан де Боттон, технический директор

Неизменяемость - это представление о том, что что-то нельзя изменить. В последнее время он стал модным словом в сообществе разработчиков из-за его повсеместного распространения на современных языках, таких как ClosureScript и Elm. Зачем вам данные, которые никогда не могут измениться, если в современных приложениях данные постоянно меняются? Одна из самых сложных проблем, которую нужно решить, - это определить, когда данные были обновлены. Как только вы это узнаете, вы сможете обновить все элементы, на которые нужно ответить.

Давайте посмотрим на распространенный случай в JavaScript, когда мы хотим проверить, изменилось ли значение.

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

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

Пока нет проблем, правда? С примитивами в JavaScript, когда мы объявляем переменную, эта переменная фактически замещает свое значение. Когда мы меняем значение, переменная изменяется. Сделав еще один шаг, изменение его значения изменяет саму переменную.

Это не относится к примитивам в JavaScript. Давайте посмотрим, как это работает с объектами.

Несмотря на то, что мы изменили значение data2, JavaScript все равно оценил объекты как истинные. Фактически, если бы вы проверили данные, вы бы обнаружили, что их значение тоже изменилось.

Если в JavaScript для переменной установлено значение «непримитивный», мы не создаем переменную и не устанавливаем ее значение; мы фактически создаем ссылку на исходный объект. Сложные структуры в JavaScript размещаются в памяти, а переменные просто становятся ссылками на эти выделения памяти. Из-за этого, используя наш подход, мы никогда не сможем проверить, изменились ли данные.

Так как же решить эту проблему? Легкий. Мы просто делаем копию исходного объекта при создании нашей второй переменной, используя метод Object.assign.

Object.assign копирует свойства последующих объектов в исходный объект, который ему передается. В приведенном выше коде мы создаем совершенно новый объект, затем копируем в него свойства из данных, а затем копируем любые новые свойства, получая совершенно новый объект. Еще одним положительным моментом использования неизменяемых данных является то, что наши данные становятся более понятными. Теперь мы знаем, что данные отличаются от данных2 и чем они отличаются. В качестве альтернативы, если вы просто хотите создать точную копию объекта, вы также можете использовать метод Object.create.

Аналогичным образом, имея дело с массивами, вы обнаружите следующее.

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

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

Однако есть и другие способы справиться с неизменяемостью. Теперь давайте посмотрим на использование современного JavaScript, или ES6, с его оператором распространения. Оператор распространения или предшествующее ему «…» предписывает JavaScript объединить свойства другого не примитивного объекта с аналогичным типом.

Это работает необычайно хорошо, а синтаксис намного естественнее, чем при использовании Object.assign или [] .concat, но он становится тупым при работе с более сложными данными.

Мы обнаруживаем, что постоянно распространяемся на разные уровни данных. Для меня это кажется слишком многословным, особенно когда я имею дело с такой простой задачей, как обновление одного свойства в записи. Существует библиотека под названием Immutable.js, написанная Facebook, которая предоставляет очень надежный набор помощников по неизменяемости. Вот как мы могли бы решить ту же проблему с Immutable.js:

На мой взгляд, такой подход гораздо удобнее для чтения. У неизменяемого объекта есть несколько методов: set, setIn, update, updateIn, merge , deepMerge, который вернет новый объект, а также обновит любые параметры, которые вы ему передали. Immutable также поддерживает гораздо больше, чем изначально предоставленные примитивы, включая Map, OrderedMap, List, Record, Seq, Collection и другие. Скорее всего, это будет излишним для многих проектов, поскольку он чрезвычайно всеобъемлющий, однако, когда вам нужно обновить и сравнить большие и сложные наборы данных, он станет бесценным.

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

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