Введение
Обсудим некоторые темы JavaScript, которые, на мой взгляд, должен знать каждый разработчик.
При работе с JavaScript всегда следует стремиться по возможности улучшить качество кода. Исходя из личного опыта, я выделил 5 тем, понимание которых помогает разработчикам избегать ошибок и принимать правильные решения в отношении кода.
#1. Promise.all() и Promise.allSettled()
Работа с промисами — неотъемлемая часть написания JavaScript-кода.
Есть много способов обращения с ними, но важно подумать о том, что подходит именно вам.
Promise.all()
Этот метод принимает итерируемый промис в качестве входного параметра и возвращает один промис. Результат каждого входного промиса будет преобразован в массив комплексных результатов.
Рассмотрим пример ниже:
const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = 23;
const allPromises = [promise1, promise2, promise3];
Promise.all(allPromises).then(values => console.log(values));
// Вывод: [ 555, 'foo', 23 ]
Если разрешаются все три промиса, разрешается и Promise.all()
, и значения будут выведены.
Но что, если один промис (или несколько) не разрешится и будет отклонен?
const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('I just got rejected!');
const allPromises = [promise1, promise2, promise3];
Promise.all(allPromises)
.then(values => console.log(values))
.catch(err => console.error(err));
// Вывод: Отклонено!
Promise.all()
отклоняется, если хотя бы один из элементов отклонен.
Как следует из приведенного выше примера, если 2 передаваемых промиса разрешаются, а один немедленно отклоняется, то Promise.all()
будет немедленно отклонен.
Promise.allSettled()
Этот метод был введен в ES2020. Promise.allSettled()
принимает в качестве входного параметра итерируемый промис, но в отличие от Promise.all()
, возвращает промис, который всегда разрешается после того, как все заданные промисы либо выполняются, либо отклоняются. Промис разрешается с помощью массива объектов, описывающих результат выполнения каждого промиса.
Для каждого результата выполнения промиса получаем одно из двух:
- статус
fulfilled
(выполнен) со значением результата; - статус
rejected
(отклонен) с указанием причины отклонения.
Рассмотрим подробнее:
const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('I just got rejected!');
const allPromises = [promise1, promise2, promise3];
Promise.allSettled(allPromises)
.then(values => console.log(values))
// Вывод:
// [
// { статус: 'fulfilled', значение: 555 },
// { статус: 'fulfilled', значение: 'foo' },
// { статус: 'rejected', причина: 'Октлонено!' }
// ]
Какой из них выбрать?
Если вы хотите, чтобы система быстро прекращала работу в случае ошибки, следует выбрать Promise.all()
.
Рассмотрим сценарий, в котором необходимо, чтобы все запросы были fulfilled
(выполнены), и определим некоторую логику, основанную на этом успехе. В этом случае быстрое прекращение работы в случае ошибки вполне подходит, поскольку после отклонения одного из запросов остальные вызовы становятся больше не актуальными. Нет необходимости тратить ресурсы на оставшиеся вызовы.
Однако в других случаях может понадобиться, чтобы все вызовы были либо rejected
(отклонены), либо fulfilled
(выполнены). Если полученные данные используются для отдельной последующей задачи, или вы хотите отобразить и получить доступ к информации об ошибках каждого вызова, лучше выбрать Promise.allSettled()
.
#2. Оператор нулевого слияния (??)
Оператор нулевого слияния обозначается двумя вопросительными знаками ??
. Этот оператор возвращает правосторонний операнд, если его левосторонний операнд равен нулю или не определен. В противном случае возвращает левосторонний операнд.
Это легко понять на простом примере. Результатом x ?? y
будет:
x
, если значениеx
не является ни null, ни undefined;y
, если значениеx
является null или undefined.
Оператор нулевого слияния применяется нечасто, особенно в среде новых разработчиков Javascript. Это просто красивый синтаксис для получения первого значения defined двух переменных.
Вы можете написать x ?? y
следующим образом:
result = (x !== null && x !== undefined) ? x : y;
Теперь вам должно быть понятно, что делает ??
.
Обычный случай использования ??
— это предоставление значения по умолчанию. Например, здесь мы отображаем name
, если значение не является null/undefined, иначе — Unknown
:
let name;
alert(name ?? "Unknown"); // Вывод: Unknown (name является undefined)
Вот пример, в котором name
присваивается левому операнду:
let name = "Michael";
alert(name ?? "Unknown"); // Michael (name не является ни null, ни undefined)
Сравнение с оператором OR “||”
Оператор OR ||
можно использовать так же, как и ??
.
Можно заменить ??
на ||
и получить тот же результат, например:
let name;
alert(name ?? "Unknown"); // Вывод: Unknown
alert(name || "Unknown"); // Вывод: Unknown
Оператор OR ||
существует с самого начала развития JavaScript, поэтому разработчики давно используют его для этих целей. Оператор ??
добавлен в JavaScript совсем недавно (ES2020), так как члены сообщества были не совсем довольны оператором ||
.
Важное различие между ними заключается в следующем:
||
возвращает первое истинное значение;??
возвращает первое значение defined (defined = не null и не undefined).
Другими словами, ||
не делает различий между false
, 0
, пустой строкой ""
и null/undefined. Все это — ложные значения. Если любое из них является первым аргументом ||
, то в качестве результата вы получите второй аргумент. Например:
let grade = 0;
alert(grade || 100); // Вывод: 100
alert(grade ?? 100); // Вывод: 0
grade || 100
проверяет, не является ли значение grade
ложным, а значение grade
равно 0, что является ложным значением. Поэтому результатом ||
будет второй аргумент — 100
. grade ?? 100
проверяет, не является ли значение grade
null или undefined, но это не так, поэтому результатом grade
остается 0
.
#3. Неправильное использование “this”
this
— часто неправильно интерпретируемое понятие в JavaScript. Чтобы применять this
в JavaScript, нужно понимать, как оно работает, потому что оно функционирует немного иначе, чем в других языках.
Вот пример распространенной ошибки при использовании this
:
const obj = {
helloWorld: "Hello World!",
printHelloWorld: function () {
console.log(this.helloWorld);
},
printHelloWorldAfter1Sec: function () {
setTimeout(function () {
console.log(this.helloWorld);
}, 1000);
},
};
obj.printHelloWorld();
// Вывод: Hello World!
obj.printHelloWorldAfter1Sec();
// Вывод: undefined
Первый результат — Hello World!
, потому что this.helloWorld
правильно указывает на свойство name объекта. Второй результат — undefined
, потому что в this
потеряна ссылка на свойства объекта.
Это происходит потому, что this
зависит от объекта, вызывающего функцию, в которой он находится. Переменная this
есть в каждой функции, но объект, на который она указывает, определяется вызывающим ее объектом.
This
в obj.printHelloWorld()
указывает непосредственно на obj
. This
в obj.printHelloWorldAfter1Sec()
указывает непосредственно на obj
. Но this
в функции обратного вызова setTimeout
не указывает ни на какой объект, потому что ни один объект не вызывается. Используется объект по умолчанию (которым является window
). name
не существует для window, что приводит к значению undefined.
Как это исправить?
Лучший способ сохранить ссылку на this
в setTimeout
— использовать стрелочные функции (которые были введены в ES6). В отличие от обычных функций, стрелочные функции не создают собственное this
.
Поэтому в следующем примере ссылка на this
сохранится.
const obj = {
helloWorld: "Hello World!",
printHelloWorld: function () {
console.log(this.helloWorld);
},
printHelloWorldAfter1Sec: function () {
setTimeout(() => {
console.log(this.helloWorld);
}, 1000);
},
};
obj.printHelloWorld();
// Вывод: Hello World!
obj.printHelloWorldAfter1Sec();
// Вывод: Hello World!
Вместо стрелочной функции, можно применить другие подходы для достижения решения. Коротко поясню суть каждого из них.
- Метод
bind()
. Создает новую функцию с заданным значениемthis
и возвращает его. Вы можете использовать этот метод для привязки функции к определенному объекту, иthis
всегда будет ссылаться на этот объект. - Методы
call()
иapply()
. Позволяют вызвать функцию с определенным значениемthis
. Разница между ними заключается в том, чтоcall()
принимает аргументы в виде списка значений, аapply()
— в виде массива. - Переменная
self
. Распространенный подход, который использовался до появления стрелочных функций. Идея заключается в том, чтобы хранить ссылку наthis
в переменной и использовать эту переменную внутри функции. Обратите внимание на то, что этот подход может давать сбой при работе с вложенными функциями.
В целом, каждый из перечисленных подходов имеет свои преимущества и недостатки, и выбор зависит от конкретной ситуации. Для большинства случаев в качестве выбора по умолчанию я все же рекомендую использовать стрелочную функцию.
#4. Чрезмерное использование памяти
Эту проблему легко распознать во время выполнения, когда вы наблюдаете за экземплярами сервера и видите, что среднее потребление памяти увеличивается. Первое, что может прийти в голову, — это увеличить лимит памяти по умолчанию в Node JS.
Но сначала стоит спросить себя: “Почему потребление памяти такое высокое?”.
На этот вопрос может быть много ответов. Рассмотрю только один распространенный случай, чтобы привлечь ваше внимание к этой проблеме.
Разберем следующий пример.
Допустим, у нас есть следующие данные:
const data = [
{ name: 'Frogi', type: Type.Frog },
{ name: 'Mark', type: Type.Human },
{ name: 'John', type: Type.Human },
{ name: 'Rexi', type: Type.Dog }
];
Нам нужно добавить некоторые свойства для каждой сущности, в зависимости от ее type
:
const mappedArr = data.map((entity) => {
return {
...entity,
walkingOnTwoLegs: entity.type === Type.Human
}
});
// ...
// другой код
// ...
const tooManyTimesMappedArr = mappedArr.map((entity) => {
return {
...entity,
greeting: entity.type === Type.Human ? 'hello' : 'none'
}
});
console.log(tooManyTimesMappedArr);
// Вывод:
// [
// { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
// { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]
Здесь показано, как с помощью map
можно выполнить простое преобразование и использовать его несколько раз. Для небольшого массива потребление памяти незначительно, но в случае больших массивов влияние на память будет очень заметным.
Так какие же решения наиболее эффективны в этом случае?
Прежде всего, вам нужно понять, что обработка больших массивов превышает сложность пространства. Затем подумайте, как можно уменьшить потребление памяти. В нашем случае есть несколько продуктивных решений.
- Соединить maps в цепочку, избегая многократного клонирования:
const mappedArr = data
.map((entity) => {
return {
...entity,
walkingOnTwoLegs: entity.type === Type.Human
}
})
.map((entity) => {
return {
...entity,
greeting: entity.type === Type.Human ? 'hello' : 'none'
}
});
console.log(mappedArr);
// Вывод:
// [
// { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
// { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]
2. Еще лучше было бы уменьшить количество map
и операций клонирования:
const mappedArr = data.map((entity) =>
entity.type === Type.Human ? {
...entity,
walkingOnTwoLegs: true,
greeting: 'hello'
} : {
...entity,
walkingOnTwoLegs: false,
greeting: 'none'
}
);
console.log(mappedArr);
// Вывод:
// [
// { name: 'Frogi', type: 'frog', walkingOnTwoLegs: false, greeting: 'none' },
// { name: 'Mark', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'John', type: 'human', walkingOnTwoLegs: true, greeting: 'hello' },
// { name: 'Rexi', type: 'dog', walkingOnTwoLegs: false, greeting: 'none' }
// ]
#5. Map/объектный литерал вместо оператора switch
Нам нужно вывести города в зависимости от стран, в которых они находятся. Посмотрим на пример ниже:
function findCities(country) {
// Используйте switch, чтобы найти города по странам
switch (country) {
case 'Russia':
return ['Moscow', 'Saint Petersburg'];
case 'Mexico':
return ['Cancun', 'Mexico City'];
case 'Germany':
return ['Munich', 'Berlin'];
default:
return [];
}
}
console.log(findCities(null)); // Вывод: []
console.log(findCities('Germany')); // Вывод: ['Munich', 'Berlin']
Кажется, что в приведенном выше коде нет ничего плохого, но по моему мнению, он выполнен в стиле довольно “жесткого” программирования. Того же результата можно добиться с помощью объектного литерала с более чистым синтаксисом:
// Используйте объектный литерал, чтобы найти города по странам
const citiesCountry = {
Russia: ['Moscow', 'Saint Petersburg'],
Mexico: ['Cancun', 'Mexico City'],
Germany: ['Munich', 'Berlin']
};
function findCities(country) {
return citiesCountry[country] ?? [];
}
console.log(findCities(null)); // Вывод: []
console.log(findCities('Germany')); // Вывод: ['Munich', 'Berlin']
Map
— это объектный тип, введенный в ES6, который позволяет хранить пары ключ-значение. Для достижения того же результата можно использовать Map
:
// Используйте Map ,чтобы найти города по странам
const citiesCountry = new Map()
.set('Russia', ['Moscow', 'Saint Petersburg'])
.set('Mexico', ['Cancun', 'Mexico City'])
.set('Germany', ['Munich', 'Berlin']);
function findCities(country) {
return citiesCountry.get(country) ?? [];
}
console.log(findCities(null)); // Вывод: []
console.log(findCities('Germany')); // Вывод: ['Munich', 'Berlin']
Значит ли это, что нужно перестать использовать оператор switch
? Я этого не утверждаю. Лично я считаю, что Map и объектные литералы повышают уровень кода и делает его более элегантным (если можно их использовать).
Основные различия между Map и объектным литералом:
- Ключи. В
Map
ключи могут быть любого типа данных (включая объекты и примитивы). В объектном литерале ключи должны быть строками или символами. - Итерация. В
Map
можно легко перебирать записи с помощью циклаfor…of
или методаforEach()
. В объектном литерале для итерации по ключам, значениям или записям необходимо использоватьObject.keys()
,Object.values()
илиObject.entries()
. - Производительность. В целом,
Map
работает лучше, чем объектные литералы, когда речь идет о больших наборах данных или частых добавлениях/удалениях. Однако в случае небольших наборов данных или нечастых операций разница в производительности незначительна.
Выбор типа структуры данных зависит от конкретного случая использования, но и Map
, и объектные литералы являются полезными структурами данных.
Заключение
В этой статье я попытался охватить некоторые важные вопросы, которые должны быть полезны как начинающим, так и опытным разработчикам JavaScript.
Конечно, существует гораздо больше идей, чем перечислено выше. Поэтому старайтесь быть в курсе инструментов и лучших практик разработки JavaScript.
Читайте также:
- 18 продвинутых навыков JavaScript для старших инженеров-программистов
- На что способен Selenium в паре с JavaScript?
- Продвинутая версия Hello World для A-Frame
Читайте нас в Telegram, VK и Дзен
Перевод статьи Ohad Koren: 5 Essential Points You Should Be Familiar With as a JavaScript Developer