DevGang
Авторизоваться

Все об иммутабелных массивах и объектах в JavaScript

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

Обычно мы описываем состояния через объекты и массивы:

const state = {
 userName: 'jdoe',
 favouriteColours: ['blue', 'orange', 'green'],
 company: 'UltimateCourses',
 skills: ['javascript', 'react', 'vue', 'angular', 'svelte']
};

Даже простые изменения состояния, обычно выполняемые с двусторонним связыванием (например, v-model в Vue или ngModel в Angular), могут извлечь пользу из неизменяемого подхода. Мы собираем данные, делая копию ввода компонента, изменяем ее и отправляем мутированную копию вызывающей стороне. Это в значительной степени снижает вероятность побочных эффектов.

Действие общего состояния - добавить или удалить элементы из массива или добавить или удалить поля из объекта. Однако стандартные операции изменяют исходный объект. Давайте посмотрим, как мы можем применять их иммутабелным образом. Наша цель - создать новый объект, а не менять существующий. Для простоты мы будем использовать оператор «spread» и деструктуризацию, представленные в ES6, но все это возможно (хотя и не так элегантно) с функциями ES5.

Иммутабелные операции с массивами

Массив имеет несколько изменяемых операций - push, pop, splice, shift, unshift, reverse и sort. Их использование обычно вызывает побочные эффекты и ошибки, которые трудно отследить. Вот почему важно использовать иммутабелный способ.

Push

Push - это операция, которая добавляет новый элемент в конец массива.

const fruits = ['orange', 'apple', 'lemon'];
fruits.push('banana'); // = ['orange', 'apple', 'lemon', 'banana']

Полученный массив является объединением исходного массива и элемента. Давайте попробуем сделать это иммутабелным способом:

const fruits = ['orange', 'apple', 'lemon'];
const newFruits = [...fruits, 'banana']; // = ['orange', 'apple', 'lemon', 'banana']

Здесь оператор распространения ... «распространяет» элементы массива в качестве аргументов.

Unshift

Unshift - это операция, похожая на push. Однако вместо добавления элемента в конце мы добавим элемент в начале массива.

const fruits = ['orange', 'apple', 'lemon'];
fruits.unshift('banana'); // = ['banana', 'orange', 'apple', 'lemon']

Точно так же мы будем использовать операцию распространения для достижения неизменности, но с небольшой модификацией:

const fruits = ['orange', 'apple', 'lemon'];
const newFruits = ['banana', ...fruits]; // = ['banana', 'orange', 'apple', 'lemon']

Pop

Pop - это операция, которая удаляет последний элемент из конца массива и возвращает его.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
const lastFruit = fruits.pop(); // = 'banana', fruits = ['orange', 'apple', 'lemon']

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

const fruits = ['orange', 'apple', 'lemon', 'banana'];
const lastFruit = fruits[fruits.length - 1]; // = 'banana'
const newFruits = fruits.slice(0, fruits.length - 1); // = ['orange', 'apple', 'lemon']

Shift

Shift - это операция, похожая на pop, но вместо удаления элемента из конца мы удаляем элемент из начала массива.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit = fruits.shift(); // = 'orange', fruits = ['apple', 'lemon', 'banana']

Наше иммутабелное решение эквивалентно подходу с методом pop. Нам не нужно указывать длину массива в операции slice, если мы хотим принять все элементы до конца.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
const firstFruit = fruits[0]; // = 'orange'
const newFruits = fruits.slice(1); // = ['apple', 'lemon', 'banana']

Удаление и вставка элементов

Чтобы добавить или удалить элемент из массива, мы обычно используем splice.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
fruits.splice(1, 2, 'strawberry'); // = ['orange', 'strawberry', 'banana']

В сочетании slice и spread дает нам тот же результат, но иммутабелным образом:

Sort и reverse

Sort и reverse являются операторами, которые, соответственно, сортируют и инвертируют порядок элементов массива.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
fruits.sort(); // = ['apple', 'banana', 'lemon', 'orange'];
fruits.reverse(); // = ['orange', 'lemon', 'banana', 'apple'];

И sort и reverse изменяют исходный массив. Однако, используя spread, мы можем сделать копию массива, чтобы произошла мутация в копии, а не в исходном массиве.

const fruits = ['orange', 'apple', 'lemon', 'banana'];
const sorted = [...fruits].sort(); // = ['apple', 'banana', 'lemon', 'orange'];
const inverted = [...fruits].reverse(); // = ['banana', 'lemon', 'apple', 'orange'];
const sortedAndInverted = [...sorted].reverse(); // = ['orange', 'lemon', 'banana', 'apple'];

Благодаря иммутабелности мы можем теперь отделить сортировку от инверсии. В результате у нас есть все четыре варианта (включая исходный массив).

Операции с иммутабелным объектом

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

Изменить и / или добавить свойство

Допустим, мы хотим изменить выбранный фрукт и установить новое количество. Стандартный способ сделать это - изменить объект.

const state = {
 selected: 'apple',
 quantity: 13,
 fruits: ['orange', 'apple', 'lemon', 'banana']
};
state.selected = 'orange';
state.quantity = 5;
state.origin = 'imported from Spain';
/* 
state = {
 selected: 'orange',
 quantity: 5,
 fruits: ['orange', 'apple', 'lemon', 'banana'],
 origin: 'imported from Spain'
}
*/

Опять же, мы можем использовать оператор spread, чтобы создать копию объекта с измененными полями. Здесь он работает подобно методу push у массива, распределяет пары ключ-значение исходного объекта на новый. Следующими двумя строками мы переопределяем значения из исходного объекта. Последняя строка создает новое поле с именем «origin».

const state = {
 selected: 'apple',
 quantity: 13,
 fruits: ['orange', 'apple', 'lemon', 'banana']
};
const newState = {
 ...state,
 selected: 'orange',
 quantity: 5,
 origin: 'imported from Spain'
};
/* 
newState = {
 fruits: ['orange', 'apple', 'lemon', 'banana'],
 selected: 'orange',
 quantity: 5,
 origin: 'imported from Spain'
}
*/

Удалить свойство

Чтобы удалить изменяемое свойство объекта, мы просто вызовем delete:

const state = {
 selected: 'apple',
 quantity: 13,
 fruits: ['orange', 'apple', 'lemon', 'banana']
};
delete state.quantity;
/* 
state = {
 selected: 'apple',
 fruits: ['orange', 'apple', 'lemon', 'banana']
} 
*/

Для удаления свойства иммутабелным способом требуется небольшая хитрость, обеспечиваемый оператором rest. Оператор rest записывается так же, как и spread - с .... Однако смысл в данном случае не в том, чтобы распространять все поля, а в том, чтобы не добавлять лишние.

const state = {
 selected: 'apple',
 quantity: 13,
 fruits: ['orange', 'apple', 'lemon', 'banana']
};
const { quantity, ...newState } = state;
/* 
quantity = 13
newState = {
 selected: 'apple',
 fruits: ['orange', 'apple', 'lemon', 'banana']
}
*/

Этот метод называется деструктурирующим присваиванием. Мы присваиваем пару ключ-значение константе quantity, а остальную часть объекта присваиваем newState.

Сложные структуры

Сложные структуры имеют вложенные массивы или объекты. В следующем примере есть state с вложенным массивом gang.

const state = {
 selected: 4,
 gang: [
   'Mike',
   'Dustin',
   'Lucas',
   'Will',
   'Jane'
 ]
};
const newState = { ...state };
newState.selected = 11;
newState.gang.push('Max');
newState.gang.push('Suzie');
/* 
state = {
 selected: 4,
 gang: [
   'Mike',
   'Dustin',
   'Lucas',
   'Will',
   'Jane'
   'Max',
   'Suzie'
 ]
}
newState = {
 selected: 11,
 gang: [
   'Mike',
   'Dustin',
   'Lucas',
   'Will',
   'Jane'
   'Max',
   'Suzie'
 ]
}
state.gang === newState.gang
*/

Не то, что мы ожидали, верно? Выполнение операции spread над сложными структурами делает только поверхностную (первый уровень) копию структуры. Здесь он только скопировал ссылку на массив gang, а не весь массив целиком. Добавление новых элементов в массив влияло как на state, так и на newState. Для решения этой проблемы нам нужно скопировать массив отдельно.

const newState = { 
 ...state, 
 gang: [...state.gang] 
};

Однако gang также может иметь сложную структуру (например, массив объектов). Если мы изменим один из объектов под ним, он изменится в обоих массивах.

const state = {
 selected: 4,
 gang: [
   { id: 1, name: 'Mike' },
   { id: 2, name: 'Dustin' },
   { id: 3, name: 'Lucas' },
   { id: 4, name: 'Will' },
   { id: 11, name: 'Jane' }
 ]
}
const newState = {
 selected: 11,
 gang: [...state.gang]
}
newState.gang[4].name = 'Eleven';
/* 
state = {
 selected: 4,
 gang: [
   { id: 1, name: 'Mike' },
   { id: 2, name: 'Dustin' },
   { id: 3, name: 'Lucas' },
   { id: 4, name: 'Will' },
   { id: 11, name: 'Eleven' }
 ]
}
newState = {
 selected: 11,
 gang: [
   { id: 1, name: 'Mike' },
   { id: 2, name: 'Dustin' },
   { id: 3, name: 'Lucas' },
   { id: 4, name: 'Will' },
   { id: 11, name: 'Eleven' }
 ]
}
*/

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

Вызов JSON.parse(JSON.stringify(obj)) делает глубокий клон объекта. Он преобразует объект в строковое представление, а затем анализирует его обратно в новый объект. Все ссылки с оригинального объекта остаются нетронутыми.

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

Заключение

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

Помните, что неизменяемые операции воссоздают массив или объект каждый раз. Если вы имеете дело с большими объектами или коллекциями, это может быть не идеальным способом обработки ваших данных. Существуют некоторые библиотеки, которые специализируются на быстрых неизменяемых операциях (например, Immutable JS или Immer), поэтому, если вы столкнетесь с препятствиями производительности с иммутабелными операциями, обязательно ознакомьтесь с ними.

#JavaScript
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу