- PVSM.RU - https://www.pvsm.ru -

Неизменяемый JavaScript: как это делается с ES6 и выше

Здравствуйте, уважаемые читатели. Сегодня мы хотели бы предложить вам перевод статьи о неизменяемости в современном JavaScript. Подробнее о различных возможностях ES6 рекомендуем [1] почитать в вышедшей у нас замечательной книге Кайла Симпсона "ES6 и не только [2]".

Писать неизменяемый код Javascript – правильно. Существует ряд потрясающих библиотек, например, Immutable.js [3], которые могли бы для этого пригодиться. Но можно ли сегодня обойтись без библиотек – писать на «ванильном» JavaScript нового поколения?

Если коротко — да. В ES6 и ES.Next есть ряд потрясающих возможностей, позволяющих добиться неизменяемого поведения без какой-либо возни. В этой статье я расскажу, как ими пользоваться – это интересно!

ES.Next – это следующ(ая/ие) верси(я/и) EcmaScript. Новые релизы [4] EcmaScript выходят ежегодно и содержат возможности, которыми можно пользоваться уже сегодня при помощи транспилятора, например, Babel [5].

Проблема

Для начала определимся, почему неизменяемость так важна? Ну, если изменять данные, то может получиться сложночитаемый код, подверженный ошибкам. Если речь идет о примитивных значениях (например, числах и строках), писать «неизменяемый» код совсем просто – ведь сами эти значения изменяться не могут. Переменные, содержащие примитивные типы, всегда указывают на конкретное значение. Если передать его другой переменной, то другая переменная получит копию этого значения.

С объектами (и массивами) другая история: они передаются по ссылке. Это означает, что, если передать объект другой переменной, то обе они будут ссылаться на один и тот же объект. Если же вы впоследствии измените объект, принадлежащий любой из них, то изменения отразятся на обеих переменных. Пример:

const person = {
  name: 'John',
  age: 28
}
const newPerson = person
newPerson.age = 30
console.log(newPerson === person) // истина
console.log(person) // { name: 'John', age: 30 }

Видите, что происходит? Изменив newObj, мы автоматически поменяем и старую переменную obj. Все потому, что они ссылаются на один и тот же объект. В большинстве случаев такое поведение нежелательно, и писать код таким образом плохо. Посмотрим, как можно решить эту проблему.

Неизменяемый JavaScript: как это делается с ES6 и выше - 1

Обеспечиваем неизменяемость

А что если не передавать объект и не изменять его, а вместо этого создавать совершенно новый объект:

const person = {
  name: 'John',
  age: 28
}
const newPerson = Object.assign({}, person, {
  age: 30
})
console.log(newPerson === person) // ложь
console.log(person) // { name: 'John', age: 28 }
console.log(newPerson) // { name: 'John', age: 30 }

Object.assign – это возможность ES6, позволяющая принимать объекты в качестве параметров. Она объединяет все передаваемые ей объекты с первым. Возможно, вы удивились: а почему первый параметр – это пустой объект {}? Если бы первым шел параметр ‘person’, то мы по-прежнему изменяли бы person. Если бы у нас было написано { age: 30 }, то мы бы опять перезаписали 30 значением 28, так как оно шло бы позже. Наше решение работает — person сохранилось без изменений, так как мы поступили с ним как с неизменяемым!

Хотите без лишних хлопот опробовать эти примеры? Открывайте JSBin [6]. В левой панели щелкните Javascript и замените его на ES6/Babel. Все, уже можете писать на ES6 :).

Однако, на самом деле в EcmaScript есть специальный синтаксис, еще сильнее упрощающий такие задачи. Он называется object spread, использовать его можно при помощи транспилятора Babel. Смотрите:

const person = {
  name: 'John',
  age: 28
}
const newPerson = {
  ...person,
  age: 30
}
console.log(newPerson === person) // ложь
console.log(newPerson) // { name: 'John', age: 30 }

Тот же результат, только теперь Код еще чище. Сначала оператор ‘spread’ (...) копирует все свойства из person в новый объект. Затем мы определяем новое свойство ‘age’, которым перезаписываем старое. Соблюдайте порядок: если бы age: 30 было определено выше person, то затем оно было бы перезаписано age: 28.

А если нужно убрать элемент? Нет, удалять мы его не будем, ведь при этом объект вновь бы изменился. Такой прием немного сложнее, и мы могли бы поступить, например, вот так:

const person = {
  name: 'John',
  password: '123',
  age: 28
}
const newPerson = Object.keys(person).reduce((obj, key) => {
  if (key !== property) {
    return { ...obj, [key]: person[key] }
  }
  return obj
}, {})

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

Массивы

Небольшой пример: как добавить элемент в массив, изменяя его:

const characters = [ 'Obi-Wan', 'Vader' ]
const newCharacters = characters
newCharacters.push('Luke')
console.log(characters === newCharacters) // истина :-(

Та же проблема, что и с объектами. Нам решительно не удалось создать новый массив, мы просто изменили старый. К счастью, в ES6 есть оператор spread для массива! Вот как его использовать:

const characters = [ 'Obi-Wan', 'Vader' ]
const newCharacters = [ ...characters, 'Luke' ]
console.log(characters === newCharacters) // false
console.log(characters) // [ 'Obi-Wan', 'Vader' ]
console.log(newCharacters) // [ 'Obi-Wan', 'Vader', 'Luke' ]

Как же просто! Мы создали новый массив, в котором содержатся старые символы плюс ‘Luke’, а старый массив не тронули.

Рассмотрим, как делать с массивами другие операции, не изменяя исходного массива:

const characters = [ 'Obi-Wan', 'Vader', 'Luke' ]
// Удаляем Вейдера
const withoutVader = characters.filter(char => char !== 'Vader')
console.log(withoutVader) // [ 'Obi-Wan', 'Luke' ]
// Меняем Вейдера на Энекина
const backInTime = characters.map(char => char === 'Vader' ? 'Anakin' : char)
console.log(backInTime) // [ 'Obi-Wan', 'Anakin', 'Luke' ]
// Все символы в верхнем регистре
const shoutOut = characters.map(char => char.toUpperCase())
console.log(shoutOut) // [ 'OBI-WAN', 'VADER', 'LUKE' ]
// Объединяем два множества символов
const otherCharacters = [ 'Yoda', 'Finn' ]
const moreCharacters = [ ...characters, ...otherCharacters ]
console.log(moreCharacters) // [ 'Obi-Wan', 'Vader', 'Luke', 'Yoda', 'Finn' ]

Видите, какие приятные «функциональные» операторы? Действующий в ES6 синтаксис стрелочных функций их только красит. Каждый раз при запуске такой функции такая функция возвращает новый массив, одно исключение – древний метод сортировки:

const characters = [ 'Obi-Wan', 'Vader', 'Luke' ]
const sortedCharacters = characters.sort()
console.log(sortedCharacters === characters) // истина :-(
console.log(characters) // [ 'Luke', 'Obi-Wan', 'Vader' ]

Да, знаю. Я считаю, что push и sort должны действовать точно как map, filter и concat, возвращать новые массивы. Но они этого не делают, и если что-то поменять, то, вероятно, можно сломать Интернет. Если вам требуется сортировка, то, пожалуй, можно воспользоваться slice, чтобы все получилось:

const characters = [ 'Obi-Wan', 'Vader', 'Luke' ]
const sortedCharacters = characters.slice().sort()
console.log(sortedCharacters === characters) // false :-D
console.log(sortedCharacters) // [ 'Luke', 'Obi-Wan', 'Vader' ]
console.log(characters) // [ 'Obi-Wan', 'Vader', 'Luke' ]

Остается ощущение, что slice() – немного «хак», но он работает.
Как видите, неизменяемость легко достигается при помощи самого обычного современного JavaScript! В конце концов, важнее всего – здравый смысл и понимание, что именно делает ваш код. Если программировать неосторожно, JavaScript может быть непредсказуем.

Замечание о производительности

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

Одна из наиболее сложных операций в JavaScript – это отслеживание изменений объекта. Решения вроде Object.observe(object, callback) довольно тяжеловесны. Однако, если держать состояние неизменяемым, то можно обойтись oldObject === newObject и таким образом проверять, не изменился ли объект. Такая операция не так сильно нагружает CPU.

Второе важное достоинство – улучшается качество кода. Когда нужно гарантировать неизменяемость состояния, приходится лучше продумывать структуру всего приложения. Вы программируете «функциональнее», весь код проще отслеживать, а гнусные баги в нем заводятся реже. Куда ни кинь – всюду вин, верно?

Для справки

» Таблица совместимости ES6: [7]
» Таблица совместимости ES.Next: [7]

Автор: Издательский дом «Питер»

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/219048

Ссылки в тексте:

[1] рекомендуем: https://habrahabr.ru/company/piter/blog/309298/

[2] ES6 и не только: http://www.piter.com/collection/all/product/es6-i-ne-tolko

[3] Immutable.js: https://facebook.github.io/immutable-js/

[4] Новые релизы: http://www.2ality.com/2015/11/tc39-process.html

[5] Babel: http://babeljs.io

[6] JSBin: http://jsbin.com/?js,console

[7] Таблица совместимости ES6:: http://kangax.github.io/compat-table/esnext/

[8] Источник: https://habrahabr.ru/post/317248/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best