В этой статье мы поделимся 5 практическими навыками разработки на JavaScript!
1.Promise.all()、Promise.allSettled()
Мы можем использовать Promise, async/await для обработки асинхронных запросов. При одновременной обработке асинхронных запросов для этого можно использовать Promise.all() и Promise.allSettled().
Обещание.все()
Статический метод Promise.all() принимает итерируемый Promise в качестве входных данных и возвращает Promise. Когда все входные Promise будут выполнены, возвращенный Promise также будет выполнен (даже если передана пустая итерация), и будет возвращен массив, содержащий все выполненные значения. Если какое-либо из введенных обещаний будет отклонено, возвращенное обещание будет отклонено с указанием причины первого отклонения.
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('rejected!');
const allPromises = [promise1, promise2, promise3];
Promise.all(allPromises)
.then(values => console.log(values))
.catch(err => console.error(err));
// 输出结果: rejected!
Promise.all() отклоняется, если отклонен хотя бы один из его элементов. В приведенном выше примере, если переданы два решенных промиса и один промис, который немедленно отклонен, то Promise.all() будет немедленно отклонен.
Обещание.allSettled()
Метод Promise.allSettled() был представлен в ES2020. Он принимает в качестве входных данных итерацию промисов и, в отличие от Promise.all(), возвращает промис, который всегда разрешается после того, как все данные промисы решены или отклонены. Промис разрешается с помощью массива объектов, описывающих результат каждого промиса.
Для результата каждого промиса существует два возможных состояния:
- выполнено: значение, содержащее результат.
- отклонено: содержит причину отклонения.
const promise1 = Promise.resolve(555);
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'foo'));
const promise3 = Promise.reject('rejected!');
const allPromises = [promise1, promise2, promise3];
Promise.allSettled(allPromises)
.then(values => console.log(values))
// 输出结果:
// [
// { status: 'fulfilled', value: 555 },
// { status: 'fulfilled', value: 'foo' },
// { status: 'rejected', reason: 'rejected!' }
// ]
Так как же выбрать между двумя методами? Если вы хотите «быстро выйти из строя», вам следует выбрать Promise.all(). Рассмотрим сценарий, в котором вам нужно, чтобы все запросы были успешными, а затем определите некоторую логику, основанную на этом успехе. В этом случае быстрый сбой приемлем, поскольку после сбоя одного запроса результаты других запросов не имеют значения, и вы не хотите тратить ресурсы на оставшиеся запросы.
В остальных случаях желательно, чтобы все запросы либо отклонялись, либо разрешались. Promise.allSettled() — правильный выбор, если полученные данные используются для последующих задач или если вы хотите отображать и получать доступ к сообщениям об ошибках для каждого запроса.
2. Оператор объединения нулей: ??
Оператор объединения значений NULL возвращает правый операнд, если левый операнд имеет значение NULL или неопределен, в противном случае возвращает левый операнд. Это удобный синтаксис для получения значения первой «определенной» из двух переменных.
Например, результат x??y:
- Возвращает x, если x не является нулевым или неопределенным.
- Если x имеет значение NULL или не определено, возвращается y.
Итак, x??y можно записать как:
result = (x !== null && x !== undefined) ? x : y;
Обычно знак ?? используется для указания значения по умолчанию. Например, в следующем примере, если значение имени не равно нулю или не определено, отображается его значение, в противном случае отображается «Неизвестно»:
const name = someValue ?? "Unknown";
console.log(name);
Если someValue не равно нулю или не определено, то значением name будет someValue; если someValue равно null или неопределенно, тогда значением name будет «Неизвестно».
?? против ||
Логический оператор И (||) можно использовать так же, как и оператор объединения нулей (??). Вы можете заменить ?? на || и получить тот же результат, например:
let name;
console.log(name ?? "Unknown"); // 输出结果: Unknown
console.log(name || "Unknown"); // 输出结果: Unknown
Разница между ними заключается в следующем: || возвращает первое истинное значение. ?? Возвращает первое определенное значение (определенное = не пустое или неопределенное). То есть оператор || не различает значения false, 0, "" и null/undefined, которые все являются ложными значениями. Если какой-либо из них является первым аргументом ||, то результатом будет второй аргумент. Например:
let grade = 0;
console.log(grade || 100); // 输出结果: 100
console.log(grade ?? 100); // 输出结果: 0
Grade || 100 проверяет, является ли Grade ложным значением, в то время как его значение равно 0, что действительно является ложным значением. Таким образом, результатом || является второй аргумент, равный 100. И оценка ?? 100 проверяет, является ли оценка нулевой или неопределенной, но это не так, поэтому результат оценки равен 0.
Так как же выбрать между двумя методами?
- Сценарии использования оператора объединения нулей (??):
Укажите значения по умолчанию для переменных. Если переменная может иметь значение NULL или неопределена, вы можете использовать оператор объединения значений NULL, чтобы предоставить для нее значение по умолчанию. Например: const name = inputName ?? "Unknown";
Обработка потенциально отсутствующих свойств: при доступе к свойствам объекта, если свойство может существовать, но имеет значение NULL или неопределенное, можно использовать оператор объединения NULL для предоставления значения по умолчанию. Например: const адрес = user.address??"Неизвестно";
Избегайте случаев ложных значений: оператор объединения значений Null можно использовать, когда мы хотим обрабатывать только явно определенные значения и избегать обработки ложных значений, таких как false, 0, пустая строка и т. д. Например: const value = userInputValue??0;
- Используйте сценарии логического оператора ИЛИ (||):
Предоставление альтернативных значений. Когда нам нужно выбрать допустимое значение из нескольких вариантов, мы можем использовать логический оператор ИЛИ. Например: const result = значение1 || значение2 || значение3;
Условие принятия решения: когда нам нужно проверить, истинно ли какое-либо из нескольких условий, мы можем использовать логический оператор ИЛИ. Например: if (condition1 || условие2) { // выполняем операцию
3. это
«Это» — понятие в JavaScript, которое часто неправильно понимают. Чтобы правильно использовать «это» в 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 объекта. Второй результат не определен, поскольку он потерял ссылку на свойство объекта. Это связано с тем, что то, к чему относится this, зависит от объекта, вызвавшего функцию, на которую оно помещено. В каждой функции есть переменная this, но объект, на который она указывает, определяется объектом, вызвавшим ее.
В obj.printHelloWorld() это напрямую указывает на obj. В obj.printHelloWorldAfter1Sec() это напрямую указывает на obj. Однако в функции обратного вызова setTimeout это не указывает ни на какой объект, поскольку нет объекта для его вызова. Используется объект по умолчанию (обычно окно). имя не существует в окне, поэтому возвращается неопределенное.
Чтобы правильно использовать это, вам нужно знать объект, с которым он связан при вызове функции. Если вы хотите получить доступ к свойствам объекта в функции обратного вызова, вы можете использовать стрелочные функции или явно привязать правильное значение this с помощью метода привязки(), чтобы избежать ошибок.
Как исправить эту проблему? Лучший способ сохранить эту ссылку в setTimeout — использовать стрелочные функции. В отличие от обычных функций, стрелочные функции не создают свои собственные 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!
Вместо использования стрелочных функций есть другие способы решения этой проблемы.
- Используйте метод связывания(): Метод связывания() создает новую функцию и возвращает значение после указания ее значения this. Вы можете использовать его для привязки функции к определенному объекту, гарантируя, что this всегда ссылается на этот объект.
- Использование методов call() и apply(): эти два метода позволяют указать конкретное значение this для вызова функции. Разница между ними в том, что метод call() принимает в качестве аргумента массив значений, тогда как метод apply() принимает в качестве аргумента массив.
- Использование переменной self: это был распространенный подход до появления стрелочных функций. Идея состоит в том, чтобы сохранить ссылку на это в переменной и использовать эту переменную внутри функции. Обратите внимание, что этот подход может не работать с вложенными функциями.
В целом каждый метод имеет свои преимущества и недостатки, и выбор того, какой метод использовать, зависит от конкретного сценария использования. В большинстве случаев функции стрелок рекомендуются по умолчанию.
4. Использование памяти
Иногда использование памяти приложением может быть очень плохим, см. следующий пример:
const data = [
{ name: 'Frogi', type: Type.Frog },
{ name: 'Mark', type: Type.Human },
{ name: 'John', type: Type.Human },
{ name: 'Rexi', type: Type.Dog }
];
Мы хотим добавить некоторые свойства к каждой сущности в зависимости от ее типа:
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' }
// ]
Как видите, с помощью карты можно выполнить простое преобразование и использовать его несколько раз. Для небольшого массива потребление памяти незначительно, но для более крупных массивов вы определенно обнаружите значительное влияние памяти.
Итак, каковы лучшие решения в этом случае?
Во-первых, нужно понимать, что при работе с большими массивами превышена пространственная сложность. Затем подумайте, как уменьшить потребление памяти. В этом примере есть несколько хороших вариантов:
1. Используйте карту в цепочке, чтобы избежать нескольких клонов:
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. Лучший способ — уменьшить количество операций сопоставления и клонирования:
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. Используйте карту или объект вместо переключателя.
Рассмотрим следующий пример:
function findCities(country) {
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 для достижения того же результата:
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']
Так стоит ли нам прекратить использовать операторы переключения? нет. Использование объектных литералов или карт, где это возможно, может улучшить уровень кода и сделать его более элегантным.
Основные различия между литералами Map и объектными литералами заключаются в следующем:
- Ключи. В Картах ключи могут иметь любой тип данных (включая объекты и примитивные значения). В объектных литералах ключи должны быть строками или символами.
- Итерация: в Map ее можно повторять с помощью цикла for...of или метода forEach(). В литералах объектов вам нужно использовать Object.keys(), Object.values() или Object.entries() для итерации.
- Производительность. В целом карты работают лучше, чем литералы объектов, при работе с большими наборами данных или частыми добавлениями/удалениями. Для небольших наборов данных или нечастых операций разница в производительности незначительна. Выбор структуры данных для использования зависит от конкретного варианта использования.