Неизменяемость в JavaScript

от автора

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

Что такое неизменяемость?

Книжное определение изменяемости звучит следующим образом: «склонность предмета к изменениям или преобразованиям». В программировании мы используем это слово, когда подразумеваем объекты, состояние которых можно изменить с течением времени. Неизменяемое значение — это с точностью до наоборот, оно уже никогда не изменится после создания.

Если это кажется странным, то позвольте вам напомнить, что большинство из тех значений, которые мы всё время используем, в действительности неизменяемы:

var statement = "I am an immutable value"; var otherStr = statement.slice(8, 17);     

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

Строки — не единственные неизменяемые значения, встроенные в JavaScript. Числа также неизменяемы. Вы вообще можете себе представить окружение, где вычисление выражения 2 + 3 меняет значение числа 2? Это звучит абсурдно, хотя мы и делаем это всё время с нашими объектами и массивами.

В JavaScript изменяемость имеется в изобилии

В JavaScript строки и числа спроектированы быть неизменяемыми. Однако, рассмотрите следующий пример с использованием массивов:

var arr = []; var v2 = arr.push(2);  

Каково значение v2? Если бы массивы вели себя сообразно строкам и числам, то v2 содержал бы новый массив с одним элементом внутри — 2. Однако это другой случай. Вместо этого была изменена ссылка arr дабы содержать число, а v2 содержит новую длину arr.

Вообразите себе тип ImmutableArray. Его поведение, заимствуя у чисел и строк, выглядело бы следующим образом:

var arr = new ImmutableArray([1, 2, 3, 4]); var v2 = arr.push(5);   arr.toArray(); // [1, 2, 3, 4] v2.toArray();  // [1, 2, 3, 4, 5] 

Аналогичным образом неизменяемый ассоциативный массив, который можно было бы использовать вместо большинства объектов, обладал бы методами для «установки» свойств, которые ничего бы не устанавливали на самом деле, а возвращали бы новый объект с требуемыми изменениями:

var person = new ImmutableMap({name: "Chris", age: 32}); var olderPerson = person.set("age", 33);   person.toObject(); // {name: "Chris", age: 32} olderPerson.toObject(); // {name: "Chris", age: 33} 

Подобно тому как 2 + 3 не меняет значений ни 2 ни 3, празднование кем-либо своего 33го дня рождения, не отменяет той истины, что человек ранее был 32 лет отроду.

Неизменяемость в JavaScript на практике

У JavaScript (пока что) не имеется неизменяемых списков и ассоциативных массивов, поэтому сейчас нам потребуется сторонняя библиотека. Существуют 2 очень хорошие доступные библиотеки. Первая из них — Mori, которая позволяет применять постоянные структуры данных из ClojureScript, а также API поддержки в JavaScript. Второй — immutable.js, написанный разработчиками из Facebook. В этой демонстрации я буду применять immutable.js по той простой причине, что его API более знакомо JavaScript разработчикам.

В данной демонстрации мы рассмотрим принцип работы с неизменяемыми данными в Сапёре. Доска представлена неизменяемым ассоциативным массивом, в котором tiles являются наиболее интересной частью данных. Это — неизменяемый список из неизменяемых ассоциативных массивов, где каждый из последних (т. е. ассоц. масс. — прим. пер.) представляет отдельную плитку на доске. Вся конструкция инициализируется с помощью объектов и массивов JavaScript, а затем становится «бессмертной» благодаря функции fromJS из immutable.js:

function createGame(options) {   return Immutable.fromJS({     cols: options.cols,     rows: options.rows,     tiles: initTiles(options.rows, options.cols, options.mines)   }); } 

Остальная часть ядра игровой логики реализована в виде функций, которые берут эту неизменяемую структуру в качестве своего первого аргумента и возвращают новый экземпляр. Наиболее важной функцией является revealTile. При вызове она помечает плитку как открытую, чтобы открыть её. С изменяемой структурой данных, это будет очень просто:

function revealTile(game, tile) {   game.tiles[tile].isRevealed = true; } 

Но с неизменяемыми структурами, подобными предложенным выше, это становится более чем сложно:

function revealTile(game, tile) {   var updatedTile = game.get('tiles').get(tile).set('isRevealed', true);   var updatedTiles = game.get('tiles').set(tile, updatedTile);   return game.set('tiles', updatedTiles); } 

Фе! К счастью, подобные вещи — нередкое явление. Поэтому в нашем инструментарии есть метод для подобных целей:

function revealTile(game, tile) {   return game.setIn(['tiles', tile, 'isRevealed'], true); } 

Теперь функция revealTile возвращает новый неизменяемый экземпляр, в котором одна из плиток отличается от предыдущей версии. setIn null-устойчива и заполнится пустыми объектами, если какая-либо из частей ключа не существует. В случае с доской Сапёра это не желательно, поскольку отсутствующая плитка означает, что мы пытаемся открыть плитку вне доски. Это можно смягчить, используя getIn для поиска плитки перед выполнением действий над нею:

function revealTile(game, tile) {   return game.getIn(['tiles', tile]) ?     game.setIn(['tiles', tile, 'isRevealed'], true) :     game; } 

Если плитка не существует, то мы просто возвращаем существующую игру. Это было краткое знакомство с неизменяемостью на практике, если хотите разобраться тщательнее, перейдите на этот codepen, там содержится полная реализация правил игры Сапёр.

А что с производительностью?

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

Поскольку неизменяемые объекты никогда не меняются, они могут быть реализованы с помощью стратегии, называемой «общие структуры» (structural sharing), которая порождает гораздо меньшую издержку в затратах на память, чем вы могли бы ожидать. В сравнении со встроенными массивами и объектами издержка все ещё будет существовать, но она будет иметь фиксированную величину и обычно может компенсироваться другим преимуществами, доступными благодаря неизменяемости. На практике, во множестве случаев применение неизменяемых данных увеличит общую производительность вашего приложения, даже если определённые операции станут более затратными в отдельности.

Улучшенное отслеживание изменений

В любом UI фреймворке одной из самых сложных задач является поиск мутаций. Это настолько широкоизвестное испытание, что EcmaScript 7 предоставляет отдельный API дабы помочь отслеживать мутации объекта с лучшей производительностью: Object.observe(). В то время как одним людям этот API по душе, другим кажется, что это ответ не на тот вопрос. В любом случае он не решает проблему отслеживания мутаций должным образом:

var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}]; Object.observe(tiles, function () { /* ... */ });   tiles[0].id = 2; 

Мутация объекта tiles[0] не приводит в действие наш обозреватель мутаций, следовательно, предложенный механизм отслеживания мутаций не годится даже для тривиального случая применения. Каким образом неизменяемость может помочь в данной ситуации? Предположим, что у приложения состояние а, а у потенциально нового приложения состояние b:

if (a === b) {   // данные не изменились, прекратить } 

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

Выводы

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

ссылка на оригинал статьи http://habrahabr.ru/post/260319/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *