Всем привет, в программировании на js часто приходится изменять содержимое объекта, добавляя, удаляя, изменяя поля, самый простой способ это конечно же присваивание:
obj.counter = obj.counter + 1
Проблема тут в том, что мы мутируем исходный объект, а это никак не допустимо в рамках функционального программирования, да и вне его, например в редьюсере redux
вы не увидите изменений если мутировали стейт.
Нужно возвращать каждый раз новый объект, например с помощью спрэд оператора ...
в ES6
(деструктурирующее присваивание если быть точнее):
const newObject = {
...obj,
counter: obj.counter + 1,
}
Но с высокой вложенностью начинает происходить страшный ад на экране, с огромным каскадом и обилием трех точек =)
Так же в популярных либах есть хелперы в виде merge
функции, например из lodash
:
_.merge({}, obj, { counter: obj.counter + 1 })
Это работает очень просто, склеивает объекты слева на право, заменяя дублирующие ключи.
Благо у нас уже есть возможность использовать нейтив инструмент из коробки: Object.assign()
, делает тоже самое
Метод Object.assign() используется для копирования значений всех собственных перечисляемых свойств из одного или более исходных объектов в целевой объект. После копирования он возвращает целевой объект.
Писателям на реакте знакома подобная конструкция:
this.setState({ counter: this.state.counter + 1 })
// или
this.setState({ show: !this.state.show })
В целом ок, но если не нужно передать какую то вложенность объекта:
this.setState(s => ({
...s,
deep: {
counter: s.deep.counter + 1,
show: !s.deep.show,
}
}))
На мой взгляд начинает выглядеть громоздко,
В либе ramda
есть отличная функция evolve
,
Она принимает на вход 2 объекта и возвращает всегда новый;
Ее сигнатура выглядит так: {k: (v → v)} → {k: v} → {k: v}
в человеческом языке так: evolve(объектМутатор, исходныйОбъект)
Вот как она работает:
const inc = n => n + 1;
const newObj = evolve({ counter: inc }, obj)
что произошло? 1 аргументом мы передали объект, с теми ключами, значения которых мы хотим поменять, но вместо значений, мы передали наши функции первого класса, они же в свою очередь получат аргументом, значение из начального объекта(исходныйОбъект
);
Создадим еще примеров,
const add = a => b => a + b; // это мы помним
const inc = add(1) // это тоже
const dec = add(-1)
const not = b => !b; // Функция меняющая булево значение на противоположное
// not(true) = false
// not(false) = true
const state = {
counter: 0,
show: false,
}
const stateTransformer = {
counter: inc,
show: not,
}
const newState = evolve(stateTransformer, state);
// newState { counter: 1, show: true }
Функция evolve
каррированая, а это значит что можно использовать ее так: evolve(объектМутатор)(исходныйОбъект)
Для меня в реакте это означает что я могу сделать что то в этом духе:
this.setState(evolve(stateTransformer));
Давайте больше примеров:
const T = () => true; // функция возвращает true
const F = () => false; // функция возвращает false
const transformDecrease = { counter: dec };
const transformincrease = { counter: inc };
const transformToggleShow = { show: not };
const transformShow = { show: T };
const transformHide = { show: F };
const handleButtonInc = () => {
this.state(evolve(transformincrease))
}
const handleButtonDec = () => {
this.state(evolve(transformDecrease))
}
const showCounter = () => {
this.setState(evolve(transformShow))
}
// можно комбинировать трансформеры, как обычные объекты
const showAdnInc = () => {
this.setState(merge(transformShow, transformincrease));
}
// или передавать в композицию:
const x = pipe(
evolve(transformShow),
evolve(transformincrease),
)
this.setState(x);
// тоже самое, что :
this.setState(evolve(transformincrease, evolve(transformShow)))
evolve
это лишь один из способов изменить объект, простой, удобный, и красивый, опять же, по моему мнению =)