😜你所需知的 JS 新特性,高效开发必备🎉

61 阅读6分钟

伴随 ECMAScript 标准持续不断地蓬勃演进,犹如为前端开发者打开了一扇又一扇充满惊喜的大门,源源不断地带来海量实用且强大的新特性。本文为大家精心分一些堪称 “效率神器” 的 ES 新特性,助力我们的代码实现质的飞跃,变得更加简洁明了、优雅动人,同时具备极高的执行效率。

1. 操作符

1.1 可选链操作符(?.)

可选链操作符(?.)能够帮助我们告别繁琐的空值检查,避免在访问对象属性时出现 TypeError。如果对象链中的任何一个引用为 nullundefined,则表达式停止计算并返回 undefined。示例:

const user = {
  name: 'Alice',
  address: {
    street: '123 Main St',
    city: 'Anytown'
  }
};
// 之前的写法
const oldCity = user && user.address && user.address.city;

// 使用可选链操作符
const city = user.address?.city;
console.log(city); // 输出: 'Anytown'

// 如果 address 为 null 或 undefined,不会抛出错误
const userWithoutAddress = { name: 'Bob' };
const city2 = userWithoutAddress.address?.city;
console.log(city2); // 输出: undefined

1.2 空值合并操作符(??)

空值合并操作符(??)可以在表达式中使用,当左侧的操作数为 nullundefined 时,返回右侧的操作数。示例:

const foo = null ?? 'default string';
console.log(foo); // 输出: 'default string'

const bar = 0 ?? 42;
console.log(bar); // 输出: 0

1.3 逻辑赋值操作符(||=、&&=、??=)

逻辑赋值操作符(||=、&&=、??=)可以帮助我们更简洁地编写条件语句。示例:

// 使用 ||= 操作符
let a = null;
a ||= 'default value'; // 等价于 a || (a = 'default value');
console.log(a); // 输出: 'default value'

// 使用 &&= 操作符
let b = true;
b &&= 'truthy value'; // 等价于 b && (b = 'truthy value');
console.log(b); // 输出: 'truthy value'

// 使用 ??= 操作符
let c = undefined;
c ??= 'default value'; // 等价于 c ?? (c = 'default value');
console.log(c); // 输出: 'default value'

1.4 数字分隔符(_)

数字分隔符可以让我们更清晰地阅读数字,提高代码可读性。示例:

const largeNumber = 1_000_000;
console.log(largeNumber); // 输出: 1000000
const bytes = 0xFF_FF_FF_FF;

1.5 指数运算符(**)

指数运算符(**)可以用来计算一个数的幂。示例:

const square = 2 ** 3;
console.log(square); // 输出: 8

2. 数组

2.1 Array.prototype.flat() 和 Array.prototype.flatMap()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。示例:

const nested = [1, [2, 3], [4, [5, 6]]];
const result1 = nested.flat(2);  // [1, 2, 3, 4, 5, 6]
// 使用 flatMap
const nested2 = [[1], [2, 3], [4, [5, 6]]];
const result2 = nested2.flatMap(x => x[0]);
console.log(result2); // 输出: [1, 2, 4]

2.2 Array.prototype.at()

at() 方法接收一个整数值并返回该索引对应的元素,允许正数和负数。负数表示从数组末尾开始计数。示例:

const array = [5, 12, 8, 130, 44];
let index = 2;
console.log(array.at(index)); // 输出: 8
index = -2;
console.log(array.at(index)); // 输出: 130

2.3 Object.groupBy()

group() 方法将数组中的元素按照指定的分组函数进行分组,并返回一个对象,其中每个键对应一个分组。示例:

const array = [
  { type: 'fruit', name: 'apple' },
  { type: 'vegetable', name: 'carrot' },
  { type: 'fruit', name: 'banana' },
  { type: 'vegetable', name: 'onion' }
];
const grouped = Object.groupBy(array, (item) => item.type);
console.log(grouped);
// 输出:
// {
//   fruit: [
//     { type: 'fruit', name: 'apple' },
//     { type: 'fruit', name: 'banana' }
//   ],
//   vegetable: [
//     { type: 'vegetable', name: 'carrot' },
//     { type: 'vegetable', name: 'onion' }
//   ]
// }

2.4 其它数组操作方法

  • Array.prototype.toReversed(): 返回一个新数组,其中元素的顺序与原数组相反。
  • Array.prototype.toSorted(): 返回一个新数组,其中元素按照指定的比较函数进行排序。
  • Array.prototype.toSpliced(): 返回一个新数组,其中删除了指定位置的元素,并在该位置插入了新元素。 这些操作都不会改变原数组,而是返回一个新数组。示例:
const array = [1, 2, 3, 4, 5];
// 使用 toReversed()
const reversedArray = array.toReversed();
console.log(reversedArray); // 输出: [5, 4, 3, 2, 1]
// 使用 toSorted()
const sortedArray = array.toSorted((a, b) => b - a);
console.log(sortedArray); // 输出: [5, 4, 3, 2, 1]
// 使用 toSpliced()
const splicedArray = array.toSpliced(1, 2, 10, 11);
console.log(splicedArray); // 输出: [1, 10, 11, 5]

3. 对象

3.1 私有类字段(Private Class Fields)

私有类字段允许我们在类中定义私有属性和方法,这些属性和方法只能在类的内部访问,不能在类的实例上访问。示例:

class Person {
  #name;
  constructor(name) {
    this.#name = name;
  }
  greet() {
    console.log(`Hello, my name is ${this.#name}`);
  }
}
const person = new Person('Alice');
person.greet(); // 输出: Hello, my name is Alice
console.log(person.#name); // 报错: Uncaught SyntaxError: Private field '#name' must be declared in an enclosing class

Chrome 控制台中运行的代码可以访问类的私有属性。这是 JavaScript 语法限制对开发者工具的一种放宽。

3.2 对象字面量增强

提供了更简洁的对象属性和方法定义方式。

const name = 'Tom';
const age = 18;
const person = {
  name,
  age,
  sayHi() {
    console.log('Hi!');
  }
};

3.3 类静态初始化块

类静态初始化块允许你在类的定义中包含一个或多个静态初始化块。这些块在类被加载时执行,并且可以用来初始化类的静态属性或执行其他静态操作。

class MyClass {
  static staticProperty;

  static {
    // 这是一个静态初始化块
    this.staticProperty = 'Hello, Static Property!';
    console.log('Static initialization block executed');
  }

  static staticMethod() {
    console.log(this.staticProperty);
  }
}

MyClass.staticMethod(); // 输出: "Hello, Static Property!"

3.4 Object.hasOwn()

Object.hasOwn() 提供了一种更安全、更可靠的方式来检查对象是否拥有指定的属性,可以替代 Object.prototype.hasOwnProperty() 方法,因为它不会受到对象原型链上属性的影响。

const obj = { name: 'Alice', age: 30 };

console.log(Object.hasOwn(obj, 'name')); // 输出: true
console.log(Object.hasOwn(obj, 'gender')); // 输出: false

// 与 Object.prototype.hasOwnProperty() 方法的比较
console.log(obj.hasOwnProperty('name')); // 输出: true
console.log(obj.hasOwnProperty('gender')); // 输出: false

3.5 装饰器

装饰器(Decorators)是一种特殊的声明,它可以被附加到类声明、方法、访问器、属性或参数上。装饰器使用@expression的形式,其中expression必须是一个函数,它会在运行时被调用,以修改类的行为。

function logged(target, context) {
  return class extends target {
    exec(...args) {
      console.log('Starting execution...');
      const result = super.exec(...args);
      console.log('Finished execution.');
      return result;
    }
  };
}

@logged
class Example {
  exec() {
    // ...
  }
}

目前装饰器在浏览器中的支持还处于实验阶段,需要使用 Babel 或 TypeScript 等工具进行转换。

4. 异步操作Promise

4.1 Promise.allSettled()

Promise.allSettled() 方法允许同时处理多个 Promise,并在所有 Promise 都已解决(无论是成功还是失败)后返回一个结果数组。与 Promise.all() 不同,Promise.allSettled() 不会因为其中一个 Promise 失败而立即拒绝,而是会等待所有 Promise 都完成后再返回结果。

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 100, 'foo'),
);
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) =>
  results.forEach((result) => console.log(result.status)),
);

// Expected output:
// "fulfilled"
// "rejected"

4.2 Promise.any()

Promise.any() 接受一个 Promise 可迭代对象(例如数组)作为参数,并返回一个新的 Promise。这个新的 Promise 会在可迭代对象中的任何一个 Promise 成功时立即解析,其解析值为第一个成功的 Promise 的结果。

const promises = [
  Promise.reject(1),
  Promise.reject(2),
  Promise.resolve(3)
];

Promise.any(promises).then((result) => {
  console.log(result); // 输出: 3
}).catch((error) => {
  console.log(error);
});

4.3 Promise.withResolvers()

Promise.withResolvers() 同时返回 Promise 本身以及它的 resolve 和 reject 函数, 提供了一种更优雅的 Promise 控制方式。

const { promise, resolve, reject } = Promise.withResolvers();

// 模拟异步操作
setTimeout(() => {
  const randomNumber = Math.random();
  if (randomNumber > 0.5) {
    resolve(randomNumber);
  } else {
    reject(new Error('Random number is too low'));
  }
}, 1000);

// 使用 Promise
promise
  .then(result => console.log('Resolved:', result))
  .catch(error => console.error('Rejected:', error));
  // Resolved: 0.7118004612730797
  // 或
  // Rejected: Error: Random number is too low

4.4 顶层await

在模块顶层使用await,无需包含在async中,目的是借用await解决模块异步加载的问题。

// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`);
// 数据库操作
const connection = await dbConnector();
// 依赖回滚
let jQuery;
try {
  jQuery = await import('https://cdn-a.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.com/jQuery');
}

5. 其它

5.1 动态导入(Dynamic Import)

动态导入(Dynamic Import)允许在运行时动态地加载模块。这对于需要按需加载模块的情况非常有用,有利于优化页面性能。例如在单页应用程序(SPA)中,你可能只需要在用户导航到特定页面时加载该页面所需的模块。

// 动态导入一个模块
import('./module.js')
  .then(module => {
    // 使用模块的导出
    module.exportedFunction();
  })
  .catch(error => {
    // 处理导入错误
    console.error('Error importing module:', error);
  });

5.2 BigInt

BigInt 是一种新的数据类型,它提供了一种方法来表示大于 2^53 - 1 的整数,避免了 JavaScript 中整数的精度限制。

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

5.3 globalThis

globalThis 提供了一个标准的方式来获取不同环境下的全局 this 对象(也就是全局对象自身),无论当前环境是浏览器、Node.js 还是其他环境。

if (typeof globalThis.setTimeout !== "function") {
  //  此环境中没有 setTimeout 方法!
}

5.4 String.prototype.matchAll()

String.prototype.matchAll()具有更强大的字符串匹配能力,它返回一个迭代器,其中包含所有匹配正则表达式的结果,包括捕获组。

const regexp = /t(e)(st(\d?))/g;
const str = 'test1test2';

const matches = str.matchAll(regexp);

for (const match of matches) {
  console.log(match);
}
// 输出:
// ["test1", "e", "st1", "1", index: 0, input: "test1test2", groups: undefined]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2", groups: undefined]

5.5 String.prototype.replaceAll()

String.prototype.replaceAll() 方法用于在字符串中替换所有匹配正则表达式的子串。它返回一个新的字符串,其中所有匹配的子串都被替换为指定的字符串。

const str = 'Hello, world!';
const newStr = str.replaceAll('l', 'x');
console.log(newStr); // 输出: "Hexxo, worxd!"

5.6 WeakRef 和 FinalizationRegistry

WeakRefFinalizationRegistry可以帮助我们更好地管理内存和垃圾回收,提高前端应用的性能和稳定性。

// WeakRef
const obj = { name: 'Alice' };
const weakRef = new WeakRef(obj);
// 检查对象是否存在
if (weakRef.deref()) {
  console.log('Object still exists');
} else {
  console.log('Object has been garbage collected');
}
// FinalizationRegistry
const registry = new FinalizationRegistry((value) => {
  console.log(`Object ${value} has been garbage collected`);
});
const obj2 = { name: 'Bob' };
registry.register(obj2, 'Bob');
// 移除对象的注册
registry.unregister(obj2);

5.7 错误原因(Error Cause)

更好的追踪错误的原因。

try {
  // 一些可能会抛出错误的代码
  throw new Error('Something went wrong', { cause: new Error('Original error') });
} catch (error) {
  console.error('Caught error:', error.message);
  console.error('Cause:', error.cause.message);
}

5.8 正则表达式命名捕获组

Regex.exec()方法返回的结果中,捕获组的名称也被包含在结果数组中。

5.9 正则表达式命名捕获组(RegExp.prototype.exec)

RegExp.prototype.exec() 在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。

const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const str = 'Today is 2023-10-01';

const result = regex.exec(str);
console.log(result);
// 输出:
// [
//   '2023-10-01',
//   '2023',
//   '10',
//   '01',
//   index: 9,
//   input: 'Today is 2023-10-01',
//   groups: {year: '2023', month: '10', day: '01'}
// ]

总结

如有错误,欢迎指正!也欢迎大家进行补充~