五个超级好用的 JavaScript 技巧!

开发 前端
本文来分享 5 个超级实用的 JavaScript 开发技巧!

本文来分享 5 个超级实用的 JavaScript 开发技巧!

1.Promise.all()、Promise.allSettled()

我们可以使用 Promise、async/await 来处理异步请求。当并发处理异步请求时,可以使用 Promise.all() 和 Promise.allSettled() 来实现。

Promise.all()

Promise.all() 静态方法接受一个 Promise 可迭代对象作为输入,并返回一个 Promise。当所有输入的 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 都被解析时,Promise.all() 会被解析并且值会被打印出来。但是,如果有一个或多个 Promise 没有被解析而被拒绝了怎么办呢?

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 和一个立即被拒绝的 Promise,那么 Promise.all() 会立即被拒绝。

Promise.allSettled()

Promise.allSettled() 方法是在 ES2020 中引入的。它以一个包含多个 Promise 的可迭代对象作为输入参数,与 Promise.all() 不同的是,它返回一个 Promise,在所有给定的 Promise 被解析或拒绝后始终会被解析。这个 Promise 会以一个描述每个 Promise 结果的对象数组来进行解析。

对于每个 Promise 的结果,会得到以下两种可能的状态:

  • fulfilled:包含结果的值。
  • rejected:包含拒绝的原因。
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或undefined时返回右操作数,否则返回左操作数。它是一种获取两个变量中第一个“定义”的值的简洁语法。

比如,x ?? y 的结果是:

  • 如果 x 不是null或undefined,则返回 x。
  • 如果 x 是null或undefined,则返回 y。

所以,x ?? y 可以写作:

result = (x !== null && x !== undefined) ? x : y;

?? 的常见用法是提供一个默认值。 例如,下面的例子中,当 name 的值不是 null/undefined时,就显示它的值,否则显示 "Unknown":

const name = someValue ?? "Unknown";
console.log(name);

如果 someValue 不是 null 或 undefined,那么 name 的值将为 someValue;如果 someValue 是 null 或 undefined,那么 name 的值将为 "Unknown"。

?? vs ||

逻辑与运算符(||)可以与空值合并运算符(?? )以相同的方式使用。 可以用 || 替换 ??,仍然能得到相同的结果,例如:

let name;
console.log(name ?? "Unknown"); // 输出结果: Unknown
console.log(name || "Unknown"); // 输出结果: Unknown

它们之间的区别在于:|| 返回第一个真值。 ?? 返回第一个已定义的值(已定义 = 非 null 或 undefined)。也就是说,|| 运算符不区分 false、0、"" 和 null/undefined,它们都是假值。如果其中任何一个是 || 的第一个参数,那么结果将是第二个参数。例如:

let grade = 0;
console.log(grade || 100); // 输出结果: 100
console.log(grade ?? 100); // 输出结果: 0

grade || 100 检查 grade 是否是一个假值,而它的值是 0,确实是一个假值。所以 || 的结果是第二个参数,即 100。而 grade ?? 100 检查 grade 是否为 null 或 undefined,但它并不是,所以 grade 的结果为 0。

那该如何选择两个方法呢?

  • 空值合并运算符 (??) 的使用场景:

为变量提供默认值:当一个变量可能是 null 或 undefined 时,可以使用空值合并运算符为其提供默认值。 例如:const name = inputName ?? "Unknown";

处理可能缺失的属性:当访问对象属性时,如果该属性可能存在但值为 null 或 undefined,可以使用空值合并运算符提供默认值。 例如:const address = user.address ?? "Unknown";

避免出现假值的情况:当我们只想处理显式定义的值,并避免处理假值(如 false、0、空字符串等)时,可以使用空值合并运算符。 例如:const value = userInputValue ?? 0;

  • 逻辑或运算符 (||) 的使用场景:

提供备选值:当我们需要从多个选项中选择一个有效的值时,可以使用逻辑或运算符。 例如:const result = value1 || value2 || value3;

判断条件:当我们需要检查多个条件中的任一条件是否为真时,可以使用逻辑或运算符。 例如:if (condition1 || condition2) { // 执行操作 

3.this

"this" 是 JavaScript 中一个常被误解的概念。要在 JavaScript 中正确使用 "this",你需要真正理解它的工作方式,因为它与其他编程语言有一些不同之处。

下面是一个常见的在使用 "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 变量,但它指向的对象由调用它的对象确定。

在obj.printHelloWorld() 中,this 直接指向了 obj。 在 obj.printHelloWorldAfter1Sec()中,this 直接指向了 obj。 但是,在 setTimeout 的回调函数中,this 没有指向任何对象,因为没有对象调用它。默认对象(通常是 window)被使用。name 在 window 上并不存在,所以返回了 undefined。

要正确使用 this,需要了解函数调用时它所绑定的对象。如果想在回调函数中访问对象属性,可以使用箭头函数或者显式地通过 bind() 方法绑定正确的 this 值,以避免出现错误。

如何修复这个问题?要保持 setTimeout 中的 this 引用,最好的方法是使用箭头函数。与普通函数不同,箭头函数不会创建自己的 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() 方法:bind() 方法创建一个新的函数,并指定其 this 值后返回。可以使用它将函数绑定到特定的对象上,确保 this 始终引用该对象。
  • 使用 call() 和 apply() 方法:这两个方法允许指定一个特定的 this 值来调用函数。它们之间的区别在于,call() 方法接受一组值作为参数,而 apply() 方法接受一个数组作为参数。
  • 使用 self 变量:这是在引入箭头函数之前常用的一种方法。思路是将 this 的引用存储在一个变量中,并在函数内部使用该变量。需要注意的是,这种方法在嵌套函数中可能效果不佳。

总的来说,每种方法都有其优缺点,选择使用哪种方法取决于具体的使用场景。对于大多数情况,默认推荐使用箭头函数。

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' }
// ]

可以看到,通过使用 map,可以进行简单的转换并多次使用它。对于一个小数组来说,内存消耗是微不足道的,但对于较大的数组来说,肯定会发现内存的显著影响。

那么,在这种情况下有哪些更好的解决方案呢?

首先,需要理解当处理大数组时,会超出空间复杂度。然后,思考如何减少内存消耗。在这个例子中,有几个不错的选择:

  1. 链式使用 map 来避免多次克隆:
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' }
// ]
  1. 更好的方法是减少 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 或 Object 代替 switch-case

来看下面的例子:

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']

那我们是否应该停止使用 switch 语句?不是的。在可能的情况下使用对象字面量或 Map 可以提高代码水平,使其更加优雅。

Map 和对象字面量之间的主要区别如下:

  • 键: 在 Map 中,键可以是任何数据类型(包括对象和原始值)。而在对象字面量中,键必须是字符串或符号。
  • 迭代: 在 Map 中,可以使用 for...of 循环或 forEach() 方法迭代。在对象字面量中,需要使用 Object.keys()、Object.values() 或 Object.entries() 来迭代。
  • 性能: 一般来说,在处理大型数据集或频繁添加/删除时,Map 的性能优于对象字面量。对于小型数据集或不经常操作的情况下,性能差异可以忽略不计。 选择使用哪种数据结构取决于具体的用例。
责任编辑:华轩 来源: 前端充电宝
相关推荐

2023-06-28 00:02:40

2023-07-18 07:56:31

工具reduce业务

2020-12-14 08:30:02

JavaScript开发代码

2022-05-10 09:33:50

Pandas技巧代码

2021-09-22 23:17:09

Java开发数组

2020-07-02 08:27:47

Javascript

2020-06-24 07:44:12

Python数据技术

2022-11-07 16:25:07

JavaScript技巧

2023-12-19 13:31:00

CSS前端技巧

2024-02-26 08:20:00

CSS开发

2015-07-16 14:51:13

下载助手断点续传多任务

2023-05-30 15:11:16

JavaScrip开发功能

2022-12-19 15:23:51

JavaScrip开发语言

2020-09-29 08:14:46

JavaScript开发代码

2023-11-26 17:54:07

JavaScript开发

2022-12-22 14:44:06

JavaScript技巧

2022-12-25 16:03:31

JavaScript技巧

2023-08-11 17:39:43

JavaScriptWeb 应用程序

2018-04-19 14:30:25

LinuxVi编辑器

2018-09-25 11:19:05

开源JavaScript图表库
点赞
收藏

51CTO技术栈公众号