- PVSM.RU - https://www.pvsm.ru -
Здравствуйте, уважаемые читатели. Сегодня мы хотели бы предложить вам перевод статьи о неизменяемости в современном 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
. Все потому, что они ссылаются на один и тот же объект. В большинстве случаев такое поведение нежелательно, и писать код таким образом плохо. Посмотрим, как можно решить эту проблему.
А что если не передавать объект и не изменять его, а вместо этого создавать совершенно новый объект:
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
Нажмите здесь для печати.