Чтобы понять важность неизменяемости, мы должны сначала взглянуть на концепцию изменчивости. Мы должны понимать, что это такое, что это означает и каковы последствия или могут быть.

В этом посте мы рассмотрим некоторые концепции изменчивости с помощью JavaScript. Однако эти принципы не зависят от языка.

Мута… Что?

Изменчивость! По сути, концепция изменчивости описывает, можно ли изменить состояние объекта после того, как он был объявлен. Это так просто.

Учтите, что у нас есть переменная, и мы присваиваем ей значение при ее объявлении. Позже в нашем коде мы сталкиваемся со сценарием, в котором нам нужно изменить значение этой переменной. Если теперь мы продолжим и сможем изменить значение этой переменной, изменив ее состояние, объект будет считаться изменяемым.

// Original array
const foo = [ 1, 2, 3, 4, 5 ]
// Mutating original array
foo.push(6) // [ 1, 2, 3, 4, 5, 6 ]
// Original object
const bar = { becky: 'lemme' }
// Mutating original object
bar.becky = true

Когда дело доходит до массивов, очень легко изменить массив и изменить состояние его значения. Чтобы этого не произошло, чтобы мы оставались в неизменяемом состоянии, мы должны создать новый массив, производный от оригинала, со вставленным в него новым элементом.

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

А вот и ...

В JavaScript есть концепция примитивных типов, которыми являются Строка и Число. Они считаются неизменными сразу после установки. Сложность здесь заключается в том, что, хотя сама строка неизменна, присваивание переменной все еще изменяемо. Это означает, что если бы мы создали переменную и присвоили ей строку, если бы мы переназначили эту переменную новой строке, технически это изменило бы не исходную строку, а скорее назначение переменных. Это важное различие.

// Instantiate and declare variable
let foo = 'something'
// Instantiate and declare variable to existing primitive type
let bar = foo
// Reassign the value of initial variable
foo = 'else'
// Log out the results
console.log(foo, bar)
> 'else', 'something'

Примитивный тип был создан неизменно - это означает, что при создании экземпляра bar, хотя он был установлен в foo, значение в памяти сохранялось отдельно. Так бывает со всеми примитивными типами! Это приводит к тому, что новое присвоение не проникает в другие переменные, использующие его в качестве указателя!

Попробуйте неизменность размера

Обратная сторона изменчивости - неизменность. Здесь после того, как переменная была объявлена ​​и установлено состояние, ее нельзя изменить снова. Вместо этого новый объект, основанный на оригинале, необходимо создать с любыми внесенными в него изменениями.

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

const foo = [ 1, 2, 3, 4, 5 ]
// Immutable, not mutating original array (ES6 Spread)
const bar = [ ...foo, 6 ]
const arr = [ 6, ...foo ]

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

Если бы у нас был более сложный массив, такой как массив объектов, как бы мы могли изменить каждый объект, не нарушая неизменяемости? Простой! Мы могли бы использовать .map, который является встроенной функцией массива.

const foo = [{ a: 'b', c: 'd' }]
// Immutable, not mutating original array
const bar = foo.map(item => ({
  ...item,
  a: 'something else'
}))

А как насчет объектов? Как бы мы обновили свойства автономного объекта, не изменяя оригинал? И снова мы можем просто использовать синтаксис распространения.

const foo = { becky: 'lemme' }
// Immutable, not mutating original object
const bar = { ...foo, smash: false }

Исходный объект foo остается нетронутым и находится в том же состоянии, в котором мы его нашли - и мы создали новый объект с изменениями, которые хотели увидеть. Прохладный!

Однако на мгновение предположим, что мы не можем использовать стандарты ES6. Как мы могли добиться неизменности?

const foo = { becky: 'lemme' }
// Immutable, not mutating original object
const bar = Object.assign({}, foo, { smash: false })

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

NB. Имейте в виду, что использование оператора распространения на верхнем уровне вложенного объекта не гарантирует неизменность вложенных в него объектов. Как мы видим в приведенном ниже примере.

const personA = {
  address: {
   city: 'Cape Town'
  }
}
const personB = {
  ...personA
}
const personC = {
  address: {
    ...personA.address,
  }
}
personA.address.city = 'Durban' // This mutates both person A & B
console.log(personB.address.city) // 'Durban'
console.log(personC.address.city) // 'Cape Town'

Чтобы гарантировать, что вложенные объекты остаются неизменными, каждый вложенный объект должен быть распределен или назначен, как показано в приведенных выше фрагментах.

Вы ответили как, но не почему?

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

Чтобы лучше понять сказанное выше, давайте взглянем на диаграмму ниже. Предположим, что foo содержит некоторые неопределенно важные данные о пользователе нашей системы. Если у нас есть Promise A и Promise B, которые одновременно выполняются в Promise. Все и оба обещания принимают объект foo в качестве параметра, если одно из обещаний изменяет foo, то новое состояние foo просачивается во второе обещание.

Это могло потенциально вызвать осложнения при выполнении этого обещания, если бы оно полагалось на foo нахождение в исходном состоянии.

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

Это потенциально может вызвать некоторые головные боли при отладке кода или даже при попытке реализовать новую функцию. Я бы посоветовал оставаться неизменным!

Итак, я должен создать новый объект?

Короче да. Однако вам не следует просто устанавливать новую переменную напрямую на старую. Это также может вызвать осложнения и может пойти не так, как мы ожидали.

const foo = { a: 'b', c: 'd' }
// This creates a pointer or shallow copy
const bar = foo
// This creates a deep copy
const bar = { ...foo }

Разница является фундаментальной в JavaScript, особенно когда дело доходит до того, как переменные хранятся в памяти.

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

Это означает, что если foo будет мутирован, то bar также отразит эти мутации. Непредвиденные последствия!

Но как насчет производительности?

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

JavaScript использует концепцию структурного разделения, что означает, что создание нового измененного объекта, производного от вашего первого объекта, на самом деле не вызывает слишком больших накладных расходов. Если учесть это, а также преимущества, которые дает неизменность, это начинает выглядеть как серьезный вариант. Чтобы назвать несколько преимуществ…

  • Безопасность потоков (для многопоточных языков)
  • Легче тестировать и использовать
  • Неудачная атомарность
  • Снижает временную связь

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

Мы уже там?

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

Вот несколько ссылок на ресурсы, которые могут заинтересовать вас, если вы захотите узнать больше или начать внедрять неизменяемость в свою кодовую базу.

Immutable.js
Преимущества неизменности

MDN - Массивы
MDN - Объекты

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

Auf Wedersen!