Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Мутация в JavaScript – это изменение объекта или массива без создания новой переменной и переприсваивания значения. Например, вот так:
const puppy = {
name: 'Dessi',
age: 9
};
puppy.age = 10;
Оригинальный объект puppy
мутировал: мы изменили значение поля age
.
Проблемы с мутациями
Казалось бы – ничего страшного. Но такие маленькие изменения могут приводить к большим проблемам.
function printSortedArray(array) {
array.sort();
for (const item of array) {
console.log(item);
}
}
Когда мы вызываем функцию с названием printSortedArray
, то обычно не думаем о том, что она что-то сделает с полученными данными. Но здесь встроенный метод массива sort()
изменяет оригинальный массив. Так как массивы в JavaScript передаются по ссылке, то последующие операции будут иметь дело с обновленным, отсортированным порядком элементов.
Подобные ошибки трудно заметить, ведь операции выполняются нормально – только с результатом что-то не так. Функция рассчитывает на один аргумент, а получает «мутанта» – результат работы другой функции.
Решением являются иммутабельные (неизменяемые) структуры данных. Эта концепция предусматривает создание нового объекта для каждого обновления.
Если у вас есть 9
-месячный puppy
, который внезапно подрос, придется записать его в новую переменную grownUpPuppy
.
К сожалению, иммутабельность из коробки в JavaScript не поддерживается. Существующие решения – это более или менее кривые костыли. Но если вы будете максимально избегать мутаций в коде, код станет понятнее и надежнее.
const
в JavaScript защищает от изменения переменную, но не ее значение! Вы не сможете присвоить этой переменной другой объект, но вполне можете изменить поля оригинального объекта. Избегайте мутирующих операций
Проблема
Распространенная мутация в JavaScript – изменение объекта:
function parseExample(content, lang, modifiers) {
const example = {
content,
lang
};
if (modifiers) {
if (hasStringModifiers(modifiers)) {
example.settings = modifiers
.split(' ')
.reduce((obj, modifier) => {
obj[modifier] = true;
return obj;
}, {});
} else {
try {
example.settings = JSON.parse(modifiers);
} catch (err) {
return {
error: `Cannot parse modifiers`
};
}
}
}
return example;
}
В этом примере мы создаем объект с тремя полями, поле settings
опционально. Для добавления объекта мы мутируем исходный объект example
– добавляем новое свойство. Чтобы понять, как выглядит в итоге объект example
со всеми возможными вариациями, нужно просмотреть всю функцию. Было бы удобнее видеть его целиком в одном месте.
Решение
В большинстве кейсов отсутствующее поле в объекте заменимо полем со значением undefined
.
В примере также присутствует конструкция try-catch
, из которой в случае ошибки возвращается объект с совершенно другой структурой и единственным полем error
. Это особый случай – объекты абсолютно разные, нет необходимости их объединять.
Чтобы очистить код, вынесем вычисление settings
в отдельную функцию:
function getSettings(modifiers) {
if (!modifiers) {
return undefined;
}
if (hasStringModifiers(modifiers)) {
return modifiers.split(' ').reduce((obj, modifier) => {
obj[modifier] = true;
return obj;
}, {});
}
return JSON.parse(modifiers);
}
function parseExample(content, lang, modifiers) {
try {
return {
content,
lang,
settings: getSettings(modifiers)
};
} catch (err) {
return {
error: `Cannot parse modifiers`
};
}
}
Теперь проще понять и что делает фрагмент, и форму возвращаемого объекта. Благодаря рефакторингу мы избавились от мутаций и уменьшили вложенность.
Будьте осторожны с мутирующими методами массивов
Далеко не все методы в JavaScript возвращают новый массив или объект. Многие мутируют оригинальное значение прямо на месте. Например, push()
– один из самых часто используемых.
Проблема
Посмотрим на этот код:
const generateOptionalRows = () => {
const rows = [];
if (product1.colors.length + product2.colors.length > 0) {
rows.push({
row: 'Colors',
product1: <ProductOptions options={product1.colors} />,
product2: <ProductOptions options={product2.colors} />
});
}
if (product1.sizes.length + product2.sizes.length > 0) {
rows.push({
row: 'Sizes',
product1: <ProductOptions options={product1.sizes} />,
product2: <ProductOptions options={product2.sizes} />
});
}
return rows;
};
const rows = [
{
row: 'Name',
product1: <Text>{product1.name}</Text>,
product2: <Text>{product2.name}</Text>
},
// More rows...
...generateOptionalRows()
];
Здесь описаны два пути определения строк таблицы: массив с постоянными значениями и функция, возвращающая строки для опциональных данных. Внутри последней происходит мутация оригинального массива с помощью метода .push()
.
Сама по себе мутация – не такая уж большая проблема. Но где мутации, там и другие подводные камни. Проблема этого фрагмента – императивное построение массива и различные способы обработки постоянных и опциональных строк.
Решение
Одна из полезных техник рефакторинга – замена императивного кода, полного циклов и условий, на декларативный. Давайте объединим все возможные ряды в единый декларативный массив:
const rows = [
{
row: 'Name',
product1: <Text>{product1.name}</Text>,
product2: <Text>{product2.name}</Text>
},
// More rows...
{
row: 'Colors',
product1: <ProductOptions options={product1.colors} />,
product2: <ProductOptions options={product2.colors} />,
isVisible: (product1, product2) =>
(product1.colors.length > 0 || product2.colors.length) > 0
},
{
row: 'Sizes',
product1: <ProductOptions options={product1.sizes} />,
product2: <ProductOptions options={product2.sizes} />,
isVisible: (product1, product2) =>
(product1.sizes.length > 0 || product2.sizes.length) > 0
}
];
const visibleRows = rows.filter(row => {
if (typeof row.isVisible === 'function') {
return row.isVisible(product1, product2);
}
return true;
});
Данные будут выведены в случае, если метод isVisible
вернет значение true
.
Код стал читаемее и удобнее для поддержки:
- Всего один путь определения строки таблицы – не нужно решать, какой метод использовать.
- Все данные в одном месте.
- Легко редактировать строки, изменяя функцию
isVisible
.
Проблема
Вот другой пример:
const defaults = { ...options };
const prompts = [];
const parameters = Object.entries(task.parameters);
for (const [name, prompt] of parameters) {
const hasInitial = typeof prompt.initial !== 'undefined';
const hasDefault = typeof defaults[name] !== 'undefined';
if (hasInitial && !hasDefault) {
defaults[name] = prompt.initial;
}
prompts.push({ ...prompt, name, initial: defaults[name] });
}
На первый взгляд, этот код не так уж плох. Он конвертирует объект в массив prompts
путем добавления новых свойств. Но если взглянуть поближе, мы найдем еще одну мутацию внутри блока if
– изменение объекта defaults
. И вот это – уже большая проблема, которую сложно обнаружить.
Решение
Код выполняет две задачи внутри одного цикла:
- конвертация объекта
task.parameters
в массивpromts
; - обновление объекта
defaults
значениями изtask.parameters
.
Для улучшения читаемости следует разделить операции:
const parameters = Object.entries(task.parameters);
const defaults = parameters.reduce(
(acc, [name, prompt]) => ({
...acc,
[name]:
prompt.initial !== undefined ? prompt.initial : options[name]
}),
{}
);
const prompts = parameters.map(([name, prompt]) => ({
...prompt,
name,
initial: defaults[name]
}));
Другие мутирующие методы массивов, которые следует использовать с осторожностью:
Избегайте мутаций аргументов функции
Так как объекты и массивы в JavaScript передаются по ссылке, их изменение внутри функции приводит к неожиданным эффектам в глобальной области видимости.
const mutate = object => {
object.secret = 'Loves pizza';
};
const person = { name: 'Chuck Norris' };
mutate(person);
// -> { name: 'Chuck Norris', secret: 'Loves pizza' }
В этом фрагменте объект person
изменяется внутри функции mutate
.
Проблема
Подобные мутации могут быть и преднамеренными, и случайными. И то, и то приводит к проблемам:
- Ухудшается читаемость кода. Функция не возвращает значение, а изменяет один из входящих параметров, становится непонятно, как ее использовать.
- Ошибки, вызванные случайными изменениями, сложно заметить и отследить.
Рассмотрим пример:
const addIfGreaterThanZero = (list, count, message) => {
if (count > 0) {
list.push({
id: message,
count
});
}
};
const getMessageProps = (
adults,
children,
infants,
youths,
seniors
) => {
const messageProps = [];
addIfGreaterThanZero(messageProps, adults, 'ADULTS');
addIfGreaterThanZero(messageProps, children, 'CHILDREN');
addIfGreaterThanZero(messageProps, infants, 'INFANTS');
addIfGreaterThanZero(messageProps, youths, 'YOUTHS');
addIfGreaterThanZero(messageProps, seniors, 'SENIORS');
return messageProps;
};
Этот код конвертирует набор числовых переменных в массив messageProps
со следующей структурой:
[
{
id: 'ADULTS',
count: 7
},
{
id: 'SENIORS',
count: 2
}
];
Проблема в том, что функция addIfGreateThanZero
вызывает мутации массива, который мы ей передаем. Это изменение преднамеренное, оно необходимо для работы функции. Однако это не самое лучшее решение – можно создать более понятный и удобный интерфейс.
Решение
Давайте перепишем функцию, чтобы она возвращала новый массив:
const addIfGreaterThanZero = (list, count, message) => {
if (count > 0) {
return [
...list,
{
id: message,
count
}
];
}
return list;
};
Но от этой функции можно полностью отказаться:
const MESSAGE_IDS = [
'ADULTS',
'CHILDREN',
'INFANTS',
'YOUTHS',
'SENIORS'
];
const getMessageProps = (
adults,
children,
infants,
youths,
seniors
) => {
return [adults, children, infants, youths, seniors]
.map((count, index) => ({
id: MESSAGE_IDS[index],
count
}))
.filter(({ count }) => count > 0);
};
Этот код проще для понимания: в нем нет повторов и сразу понятен формат результата. Функция getMessageProps
преобразует список значений в массив определенного формата, а затем отфильтровывает элементы с нулевым значением поля count
.
Можно еще немного упростить:
const MESSAGE_IDS = [
'ADULTS',
'CHILDREN',
'INFANTS',
'YOUTHS',
'SENIORS'
];
const getMessageProps = (...counts) => {
return counts
.map((count, index) => ({
id: MESSAGE_IDS[index],
count
}))
.filter(({ count }) => count > 0);
};
Но это приводит к менее очевидному интерфейсу и не позволяет использовать автокомплит в редакторе кода.
Кроме того, создается ложное впечатление, что функция принимает любое количество аргументов и в любом порядке. Но это не так.
Вместо цепочки .map()
+ .filter()
можно использовать встроенный метод массивов .reduce()
:
const MESSAGE_IDS = [
'ADULTS',
'CHILDREN',
'INFANTS',
'YOUTHS',
'SENIORS'
];
const getMessageProps = (...counts) => {
return counts.reduce((acc, count, index) => {
if (count > 0) {
acc.push({
id: MESSAGE_IDS[index],
count
});
}
return acc;
}, []);
};
Однако код с reduce
выглядит менее очевидным и труднее читается, поэтому стоило бы остановиться на предыдущем шаге рефакторинга.
Похоже, что единственная веская причина для мутации входящих параметров внутри функции – это оптимизация производительности. Если вы работаете с огромным объемом данных, то создание нового объекта/массива каждый раз – довольно затратная операция. Но как и с любой другой оптимизацией – не спешите, убедитесь, что проблема действительно существует. Не жертвуйте чистотой и ясностью кода.
Если вам нужны мутации, сделайте их явными
Проблема
Иногда мутаций не избежать, например, из-за неудачного API языка. Один из самых популярных примеров – метод массивов .sort()
.
const counts = [6, 3, 2];
const puppies = counts.sort().map(n => `${n} puppies`);
Этот фрагмент кода создает ошибочное впечатление, что массив counts
не изменяется, а просто создается новый массив puppies
, внутри которого и происходит сортировка значений. Однако метод .sort()
сортирует массив на месте – вызывает мутацию. Если разработчик не понимает этой особенности, в программе могут возникнуть ошибки, которые будет сложно отследить.
Решение
Лучше сделать мутацию явной:
const counts = [6, 3, 2];
const sortedCounts = [...counts].sort();
const puppies = sortedCounts.map(n => `${n} puppies`);
Создается неглубокая копия массива counts
, у которой и вызывается метод sort
. Исходный массив, таким образом, остается неизменным.
Другой вариант – обернуть встроенные мутирующие операции кастомной функцией и использовать ее:
function sort(array) {
return [...array].sort();
}
const counts = [6, 3, 2];
const puppies = sort(counts).map(n => `${n} puppies`);
Также вы можете применять сторонние библиотеки, например, функцию sortBy библиотеки Lodash:
const counts = [6, 3, 2];
const puppies = _.sortBy(counts).map(n => `${n} puppies`);
Обновление объектов
В современном JavaScript появились новые возможности, упрощающие реализацию иммутабельности – спасибо spread-синтаксису. До его появления нам приходилось писать что-то такое:
const prev = { coffee: 1 };
const next = Object.assign({}, prev, { pizza: 42 });
// -> { coffee: 1, pizza: 42 }
Обратите внимание на пустой объект, передаваемый в качестве первого аргумента методу Object.assign()
. Это исходное значение, которое и будет подвергаться мутациям (цель метода assign
). Таким образом, этот метод и изменяет свой параметр, и возвращает его – крайне неудачный API языка.
Теперь можно писать проще:
const prev = { coffee: 1 };
const next = { ...prev, pizza: 42 };
Суть та же, но гораздо менее многословно и без странного поведения.
А до введения стандарта ECMAScript 2015, который подарил нам Object.assign, избежать мутаций было и вовсе почти невозможно.
В документации библиотеки Redux есть замечательная страница Immutable Update Patterns, которая описывает концепцию обновления массивов и объектов без мутаций. Эта информация полезна, даже если вы не используете Redux.
Подводные камни методов обновления
Как бы ни был хорош spread-синтаксис, он тоже быстро становится громоздким:
function addDrink(meals, drink) {
return {
...meals,
lunch: {
...meals.lunch,
drinks: [...meals.lunch.drinks, drink]
}
};
}
Чтобы изменить глубоко вложенных полей, приходится разворачивать каждый уровень объекта, иначе мы потеряем данные:
function addDrink(meals, drink) {
return {
...meals,
lunch: {
drinks: [drink]
}
};
}
В этом фрагменте кода мы сохраняем только первый уровень свойств исходного объекта, а свойства lunch
и drinks
полностью переписываются.
И spread
, и Object.assign
осуществляют неглубокое клонирование – копируются только свойства первого уровня вложенности. Так что они не защищают от мутаций вложенных объектов или массивов.
Если вам приходится часто обновлять какую-то структуру данных, лучше сохранять для нее минимальный уровень вложенности.
Пока мы ждем появления в JavaScript иммутабельности из коробки, можно упростить себе жизнь двумя простыми способами:
- Избегать мутаций.
- Упростить обновление объектов.
Отслеживание мутаций
Линтинг
Один из способов отслеживать мутации – использование линтера кода. У ESLint есть несколько плагинов, которые занимаются именно этим. Например, eslint-plugin-better-mutation запрещает любые мутации, кроме локальных переменных внутри функций. Это отличная идея, которая позволяет предотвратить множество ошибок снаружи функции, но оставит большую гибкость внутри. Однако этот плагин часто ломается – даже в простых случаях вроде мутации в коллбэке метода .forEach()
.
ReadOnly
Другой способ – пометить все объекты и массивы как доступные только для чтения, если вы используете TypeScript или Flow.
Вот пример использования модификатора readonly
в TypeScript:
interface Point {
readonly x: number;
readonly y: number;
}
Использование служебного типа Readonly
:
type Point = Readonly<{
readonly x: number;
readonly y: number;
}>;
То же самое для массивов:
function sort(array: readonly any[]) {
return [...array].sort();
}
Модификатор readonly
, и тип Readonly
защищают от изменений только первый уровень свойств, так что их нужно отдельно добавлять к вложенным структурам.
В плагине eslint-plugin-functional есть правило, которое требует везде добавлять read-only типы. Его использование удобнее, чем их ручная расстановка. К сожалению, поддерживаются только модификаторы.
Еще удобнее был бы TypeScript флаг для дефолтной установки read-only типов с возможностью отката.
Заморозка
Чтобы сделать объекты доступными только для чтения во время выполнения, можно использовать метод Object.freeze
. Он также работает только на один уровень вглубь. Для «заморозки» вложенных объектов используйте библиотеку вроде deep-freeze.
Упрощение изменений
Для достижения наилучшего результата следует сочетать технику предотвращения мутаций с упрощением обновления объектов.
Самый популярный инструмент для этого – библиотека Immutable.js:
import { Map } from 'immutable';
const map1 = Map({ food: 'pizza', drink: 'coffee' });
const map2 = map1.set('drink', 'vodka');
// -> Map({ food: 'pizza', drink: 'vodka' })
Используйте ее, если вас не раздражает необходимость изучить новый API, а также постоянно преобразовывать обычные массивы и объекты в объекты Immutable.js и обратно.
Другой вариант – библиотека Immer. Она позволяет работать с объектом привычными методами, но перехватывает все операции и вместо мутации создает новый объект.
import produce from 'immer';
const map1 = { food: 'pizza', drink: 'coffee' };
const map2 = produce(map1, draftState => {
draftState.drink = 'vodka';
});
// -> { food: 'pizza', drink: 'vodka' }
Immer также замораживает полученный объект в процессе выполнения.
Иногда в мутациях нет ничего плохого
В некоторых (редких) случаях императивный код с мутациями не так уж и плох, и переписывание в декларативном стиле не сделает его лучше. Рассмотрим пример:
const getDateRange = (startDate, endDate) => {
const dateArray = [];
let currentDate = startDate;
while (currentDate <= endDate) {
dateArray.push(currentDate);
currentDate = addDays(currentDate, 1);
}
return dateArray;
};
Здесь мы создаем массив дат в заданном диапазоне. У вас есть идеи, как можно переписать этот код без императивного цикла, переприсваивания и мутаций?
В целом этот код имеет право на существование:
- Все «плохие» операции изолированы внутри маленькой функции.
- Понятное название функции само по себе описывает, что она делает.
- Работа функции не влияет на внешнюю область видимости: она не использует глобальные переменные и не изменяет свои аргументы.
Если вы используете мутации, постарайтесь изолировать их в маленькие чистые функции с понятными именами.
Руководство к действию
- Императивный код с мутациями сложнее читать и поддерживать, чем чистый декларативный код. Поэтому – рефакторьте!
- Сохраняйте полную форму объекта в одном месте и держите ее максимально чистой.
- Отделяйте логику «ЧТО» от логики «КАК».
- Изменения входных параметров функции приводит к незаметным, но очень неприятным ошибкам, которые трудно дебажить.
- Цепочка методов
.map()
+.filter()
в большинстве случаев выглядит понятнее, чем один метод.reduce()
. - Если вам очень хочется что-то мутировать, делайте это максимально явно. Старайтесь изолировать подобные операции в функции.
- Используйте техники для автоматического предотвращения мутаций.
Комментарии