Неизменяемость в JavaScript
Перевод: Leonardo Maldonado — Immutability in JavaScript
В этой статье я собираюсь рассказать о широко используемой концепции неизменяемости в JavaScript, о том, как эта концепция может помочь вам писать более качественные приложения.
То, как мы пишем код, меняется довольно быстро — каждый день у нас появляется что-то новое, создается новая концепция, новый фреймворк или библиотека, которые помогают нам лучше выполнять конкретную задачу. Благодаря этим ежедневным изменениям мы всегда должны узнавать что-то новое — это становится частью нашей работы. В частности, при разработке JavaScript, языка, который развивается и меняется каждый день вместе с новыми технологиями, мы должны обращать внимание на то, что действительно важно в наших приложениях, а что следует исключить, чтобы найти то, что подходит для соответствующей ситуации.
С ростом популярности функционального программирования одна из тенденций, о которой много говорят, — неизменяемость. Эта концепция используется не только в функциональных языках программирования — мы можем использовать ее в любом языке, но эта концепция действительно была создана и широко распространена через функциональное программирование в сообществе разработчиков JavaScript.
Итак, давайте погрузимся в неизменяемость, особенно в JavaScript, и поймем, как она может помочь нам писать лучшие приложения, которые делают наши данные более безопасными и неизменными.
Понятие неизменяемости
Концепция неизменяемости довольно проста. По сути, неизменное значение — это то, что нельзя изменить. В частности, когда мы разрабатываем наши приложения, мы можем оказаться в некоторых ситуациях, когда мы захотим создать новый объект в нашем коде, содержащий новое свойство или значение, при этом сохраняя исходное значение. Концепция неизменяемости может помочь нам создавать новые объекты, следя за тем, чтобы мы не меняли исходное значение.
В JavaScript у нас есть примитивные типы и ссылочные типы. Примитивные типы включают числа, строки, boolean, null, undefined. И ссылочные типы включают объекты, массивы и функции.
Разница между этими типами заключается в том, что примитивные типы неизменяемы (immutable), а ссылочные типы изменяемы (mutable). Например, тип string неизменяем:
let myAge = "22"; let myNewAge = myAge; myAge = "23";
Мы просто создали две переменные и присвоили myAge переменной myNewAge. Но после того, как мы изменим значение myAge, мы увидим, что они не совпадают.
console.log(myAge === myNewAge); // false
const против let
Версия ES6 позволила нам заменить переменные в нашем коде константами с помощью ключевого слова const. Но небольшая деталь, которую могут не заметить многие разработчики, заключается в том, что ключевое слово const не создает неизменяемые переменные.
const myName = "Leonardo Maldonado";
Ключевое слово const создает только доступную только для чтения ссылку на значение, что означает, что значение нельзя переназначить. Как говорится в документации MDN:
Объявление const создает доступную только для чтения ссылку на значение. Это не означает, что хранимое в нем значение неизменяемо, просто идентификатор переменной нельзя переназначить.
Поэтому если мы попытаемся изменить значение константы, мы получим ошибку.
const myName = "Leonardo Maldonado"; myName = "Leo"; // Identifier 'myName' has already been declared
Версия ES6 также дала нам новый способ объявления переменных, который мы можем понимать как противоположность ключевому слову const. Ключевое слово let позволяет нам создавать переменные, которые являются изменяемыми, как константы, но с этим ключевым словом мы можем фактически присвоить новое значение.
let myName = "Leonardo Maldonado"; myName = "Leo"; console.log(myName) // Leo
Используя ключевое слово let, мы можем присвоить новое значение. В этом примере мы создали let
со значением Leonardo Maldonado; затем мы переназначили ему значение Leo. В этом разница между let и const.
Мы знаем, что JavaScript развивается довольно быстро, и с каждой новой версией языка мы получаем все больше удивительных функций, поэтому с годами становится легче писать лучший код JavaScript, и мы можем достичь большего с меньшим количеством кода.
Давайте теперь рассмотрим некоторые методы, которые мы можем начать использовать в наших приложениях, чтобы помочь нам использовать неизменяемость.
Объекты
Один из столпов наших приложений — объект. Мы используем объекты в каждой части наших приложений, от внешнего интерфейса до серверной части, от самого сложного компонента до самого простого.
Представим, что у нас есть объект с именем myCar, который имеет следующие свойства:
const myCar = { model: "Tesla", year: 2019, owner: "Leonardo" };
Далее, мы могли бы изменить свойство напрямую, если бы захотели, верно? Давайте сменим владельца myCar.
const myCar = { model: "Tesla", year: 2019, owner: "Leonardo" }; myCar.owner = "Lucas";
Но это плохая практика! Мы не должны напрямую изменять свойство объекта — неизменяемость работает не так. Как рекомендует документация Redux, мы всегда должны создавать измененную копию нашего объекта.
Но как мы можем это сделать? Что ж, мы могли бы использовать метод Object.assign.
Object.assign
Метод Object.assign позволяет нам копировать или передавать значения от одного объекта к другому. Он возвращает целевой объект. Вот как это работает:
Object.assign(target, source);
- Метод получает параметр, который является target, объектом, который мы хотим изменить.
- Второй параметр — это source, мы объединим исходный объект с нашим целевым объектом (target).
Давайте посмотрим на этот пример:
const objectOne = { oneName: "OB1" }; const objectTwo = { twoName: "OB2" }; const objectThree = Object.assign(objectOne, objectTwo); console.log(objectThree); // Result -> { oneName: "OB1", twoName: "OB2" }
Теперь представим, что мы хотим передать значения из определенного объекта в новую переменную. Вот как бы мы поступили:
const myName = { name: "Leonardo" }; const myPerson = Object.assign({}, myName); console.log(myPerson); // Result -> { name: "Leonardo" }
Таким образом мы копируем значения и свойства объекта myName и назначаем его нашей новой переменной myPerson.
Представим, что мы хотели скопировать все значения и свойства объекта myName, но мы также хотели добавить новое свойство к объекту myPerson. Как бы мы это сделали? Просто: передав третий параметр и передав ему наше новое свойство, в нашем случае age.
const myName = { name: "Leonardo" }; const myPerson = Object.assign({}, myName, { age: 23 }); console.log(myPerson); // Result -> { name: "Leonardo", age: 23 }
Оператор Spread
Еще один способ копирования или передачи значений другому объекту — использование оператора spread. Эта функция, которая была выпущена в версии ES6, позволяет нам создавать новый объект, копируя свойства существующего объекта. Например, если бы мы хотели скопировать объект myName в новый, мы бы сделали это следующим образом:
const myName = { name: "Leonardo" }; const myPerson = { ...myName } console.log(myPerson); // Result -> { name: "Leonardo" }
И если бы мы хотели скопировать свойства myName и добавить новое свойство к нашему новому объекту:
const myName = { name: "Leonardo" }; const myPerson = { ...myName, age: 23 } console.log(myPerson); // Result -> { name: "Leonardo", age: 23 }
Redux
Первый принцип Redux — неизменяемость, поэтому мы должны здесь упомянуть Redux. Не только потому, что это самая известная и используемая библиотека управления состоянием для приложений React, но и потому, что в ее основных идеях заложена концепция неизменности. Правильный способ использовать Redux — иметь неизменяемые редукторы (reducer).
Redux не изобретал концепцию неизменяемости — она намного старше, чем эта библиотека управления состоянием, — но мы должны признать, что с этой библиотекой многие разработчики начали использовать и говорить о неизменности.
Если вы не знаете, как работает Redux, это довольно упрощенное объяснение, чтобы вы могли понять, почему здесь важна неизменяемость:
- Redux позволяет хранить все ваши данные и состояние в одном объекте, который мы называем хранилищем (store). Это может помочь нам достичь хорошего уровня масштабируемости и поддержки. Итак, давайте представим, что у нас есть наш store, и внутри него у нас есть начальное состояние:
const initialState = { name: "Leonardo Maldonado", age: 22 }
- Если мы хотим изменить наше состояние, мы должны отправить действие (action). Действие в Redux — это объект с двумя свойствами:
- type — который описывает тип нашего действия, что именно это действие делает.
- payload — точно описывает, что следует изменить.
Итак, действие в Redux выглядит так:
const changeAge = payload => ({ type: 'CHANGE_AGE', payload })
У нас есть исходное состояние; мы создали действие, которое будет отправлено для изменения состояния; Теперь мы создадим наш редуктор (reducer) и поймем, как концепция неизменяемости используется в Redux и почему так важно иметь неизменяемые данные.
reducer
— это в основном функция, которая считывает тип отправленного действия и, в зависимости от типа действия, создает следующее состояние и объединяет полезные данные действия с новым состоянием. В нашем случае мы отправили действие с именем CHANGE_AGE, поэтому в нашей функции-редукторе у нас должен быть случай, с которым нужно иметь дело, когда это действие используется.
const initialState = { name: "Leonardo Maldonado" age: 22 } const reducer = (state = initialState, action) => { switch (action.type) { case 'CHANGE_AGE': return { ...state, age: action.payload } default: return state; } }
Здесь происходит волшебство: когда отправляется наше действие CHANGE_AGE, наш редуктор должен выполнить задачу в зависимости от типа действия. В нашем случае он изменяет возраст, но он также должен сохранить исходное значение нашего начального состояния, в нашем случае name. Очень важно сохранить исходное состояние. В противном случае мы очень легко потеряем данные, и будет очень сложно их отслеживать. Вот почему первый принцип Redux — неизменность.
Immer
Если вы занимаетесь разработкой React и не используете Redux прямо сейчас, но хотите иметь неизменяемое состояние в своем приложении, вы можете использовать библиотеку Immer. В основном так работает эта библиотека:
- У вас есть ваше текущее состояние.
- Она позволяет вам применять ваши изменения к draftState, в основной копии currentState.
- После того, как все ваши изменения будут внесены, будет создано ваше nextState на основе изменений в draftState.
Например, представим, что у нас есть текущее состояние и мы хотим добавить новый объект в этот массив. Мы бы использовали функцию produce.
import produce from "immer"; const state = [{ name: "Leonardo", age: 23 }, { name: "Lucas", age: 20 }]; const nextState = produce(state, draftState => { draftState.push({ name: "Carlos", age: 18 }) });
Обычно функции produce
получают два параметра: currentState и функцию обратного вызова, которую мы будем использовать для изменения нашего draftState. Этой функцией мы создадим наше nextState. Довольно простой, но очень мощный механизм.
Если вы работаете с React и у вас возникли проблемы с управлением состоянием в вашем приложении, я действительно рекомендую вам использовать эту библиотеку. Чтобы понять, как именно работает эта библиотека, может потребоваться некоторое время, но это сэкономит вам много времени в будущем, если ваши приложения значительно разрастутся.
Заключение
Неизменяемость — это концепция не только для JavaScript, ее можно применять на любом языке. Следует обратить внимание на то, как вы управляете своими данными, и делаете ли вы все возможное, чтобы гарантировать неизменность данных и следовать хорошей модели чистого кода.
В этой статье мы узнали о неизменяемости в JavaScript, о том, что это за концепция, о которой в прошлом году широко говорили разработчики функционального программирования, и о том, как она используется в настоящее время во многих приложениях JavaScript. Мы также узнали больше о том, как в JavaScript есть множество неизменяемых методов для добавления, редактирования и удаления данных, и как мы можем использовать JavaScript для создания неизменяемого фрагмента кода. Используя неизменяемость в своем приложении, вы увидите только положительные моменты — это улучшит ваше представление о коде и сделает его более чистым и понятным.
Итак, начните писать больше неизменяемого кода прямо сейчас и посмотрите, как это поможет улучшить ваш код!
Написана какая-то ахинея. Автор начал за здравие, задавшись вопросом почему его переменные не изменяются и закончил за упокой, зачем-то начав рассуждать о константах и способах их подмены. Но речь-то началась с переменных!