5

五个超级好用的 JavaScript 技巧!

 9 months ago
source link: https://www.51cto.com/article/763906.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文来分享 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 ||952b58f89154c70b66b0930a1d543843eb3da9.png

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

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 的性能优于对象字面量。对于小型数据集或不经常操作的情况下,性能差异可以忽略不计。 选择使用哪种数据结构取决于具体的用例。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK