众所周知,ECMAScript 的迭代是很快的,想必作为前端开发人员对近几年 ES6 的新语法已经十分熟悉了,但是 ES7 到 ES12 中一些新增的 ECMAScript 提案,可能还是没有广泛地被开发者所熟知。本文带着大家一起来了解一下 2016 到 2021 年新增的一些 ECMAScript 语法以及提案,帮助大家更好地应用于自己的项目中。
TC39 规范介绍:
Stage 0
(strawman),任何 TC39 的成员都可以提交。
Stage 1
(proposal),进入此阶段意味着这一提案被认为是正式的了,需要对此提案的场景与 API 进行详尽的描述。
Stage 2
(draft),这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。
Stage 3
(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。
Stage 4
(finished),这一阶段的提案将会被纳入到 ES 每年发布的规范之中。
ES7(ES2016)
1、Array.prototype.includes
概述
includes()
方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回true
,否则返回false
。该提案目前处于 Stage 4 阶段。
动机
使用 ECMAScript
数组时,通常需要确定数组是否包含某个元素。普遍的方法是:
if (arr.indexOf(el) !== -1) {
...
}
复制代码
严格意义来说,使用 indexOf
来确定数组中是否包含某个元素是存在问题的:
- 语义不符,我们的目的是想知道数组中是否包含某个元素,并不是想知道数组中某个元素首次出现的索引。
- 对
NaN
不生效,[NaN].indexOf(NaN) === -1
。
语法
arr.includes(valueToFind[, fromIndex])
valueToFind
:需要查找的元素值。fromIndex
:可选的。从fromIndex
索引处开始查找valueToFind
。如果为负值,则按升序从arr.length + fromIndex
的索引开始搜(即从末尾开始往前跳fromIndex
的绝对值个索引,然后往后搜寻)。默认为0
。
例子:
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
[1, 2, NaN].includes(NaN); // true
复制代码
注意事项
1、如果 fromIndex
大于等于数组的长度,则会返回 false
,且该数组不会被搜索。
const arr = ['a', 'b', 'c'];
arr.includes('c', 3); // false
arr.includes('c', 100); // false
复制代码
2、如果 fromIndex
为负值,计算出的索引将作为开始搜索 valueToFind
的位置。如果计算出的索引小于 0
,则整个数组都会被搜索。
// array length is 3
// fromIndex is -100
// computed index is 3 + (-100) = -97
const arr = ['a', 'b', 'c'];
arr.includes('a', -100); // true
arr.includes('b', -100); // true
arr.includes('c', -100); // true
arr.includes('a', -2); // false
复制代码
3、includes()
方法有意设计为通用方法。它不要求 this
值是数组对象,所以它可以被用于其他类型的对象(比如类数组对象)。下面的例子展示了在函数的 arguments
对象上调用 includes()
方法。
(function() {
console.log([].includes.call(arguments, 'a')); // true
console.log([].includes.call(arguments, 'd')); // false
})('a', 'b', 'c');
复制代码
2、Exponentiation Operator
概述
求幂运算符(
**
)返回将第一个操作数加到第二个操作数的幂的结果。它等效于Math.pow
,不同之处在于它也接受BigInt
作为操作数。该提案目前处于 Stage 3 阶段。
语法
Operator:
var1 ** var2
求幂运算符是是右结合的: a ** b ** c
等于 a ** (b ** c)
。
例子:
2 ** 3 // 8
3 ** 2 // 9
3 ** 2.5 // 15.588457268119896
10 ** -1 // 0.1
NaN ** 2 // NaN
2 ** 3 ** 2 // 512
2 ** (3 ** 2) // 512
(2 ** 3) ** 2 // 64
-(2 ** 2) // -4
(-2) ** 2 // 4
复制代码
ES8(ES2017)
1、async 和 await
概述
async
和await
关键字是基于Generator
函数的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码。
动机
简化异步编程写法。
使用 Generator 函数,依次读取两个文件:
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
复制代码
上面代码的函数 gen 可以写成 async 函数,就是下面这样:
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
复制代码
一比较就会发现,async
函数就是将 Generator
函数的星号(*
)替换成 async
,将 yield
替换成 await
。
async
函数对 Generator
函数的改进,体现在以下四点:
- 内置执行器。Generator 函数的执行必须靠执行器,所以才有了
co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。 - 更好的语义。
async
和await
,比起星号(*
)和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。 - 更广的适用性。
co
模块约定,yield
命令后面只能是Thunk
函数或Promise
对象,而async
函数的await
命令后面,可以是Promise
对象和原始类型的值(数值、字符串和布尔值等,但这时会自动转成立即resolved
的Promise
对象)。 - 返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
语法
async function getData() {
const res = await api.getTableData(); // await 异步任务
// do something
}
复制代码
例子:
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
复制代码
2、Object.values 和 Object.entries
概述
Object.values()
方法返回一个给定对象自身的所有可枚举属性值的数组,值的顺序与使用 for…in 循环的顺序相同(区别在于 for…in 循环枚举原型链中的属性)。
Object.entries()
方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for…in 循环遍历该对象时返回的顺序一致(区别在于 for…in 循环还会枚举原型链中的属性)。该提案目前处于 Stage 4 阶段。
语法
Object.values(obj)
- obj:被返回可枚举属性值的对象。
Object.entries(obj)
- obj:可以返回其可枚举属性的键值对的对象。
例子:
Object.values({a: 1, b: 2, c: 3}); // [1, 2, 3]
Object.keys({a: 1, b: 2, c: 3}); // [a, b, c]
Object.entries({a: 1, b: 2, c: 3}); // [["a", 1], ["b", 2], ["c", 3]]
复制代码
3、Object.getOwnPropertyDescriptors
概述
获取一个对象所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
该提案目前处于 Stage 4 阶段。
动机
ECMAScript 中没有一个方法能够简化两个对象之间的拷贝。如今,函数式编程和不可变对象是复杂应用程序的重要组成部分,每个框架或库都在实现自己的样板文件,以便在组合对象或原型之间正确复制属性。
回到 Object.assign
,它大多数情况下会出现一些不希望出现的行为,因为它的复制方式会吞噬行为:它直接访问属性和符号,而不是它们的描述符,丢弃可能的访问器,在组合更复杂的对象或类的原型时可能会导致危险。
检索所有描述符,无论是否可枚举,也是实现类及其原型组合的关键,因为默认情况下,它们具有不可枚举的方法和访问器。
此外,装饰器可以轻松地从一个类或 mixin 中获取所有描述符,并通过 Object.defineProperties
过滤不需要的描述符。
Object.assign
只适合做对象的浅拷贝。
语法
Object.getOwnPropertyDescriptors(obj)
- obj:任意对象
例子:
const a = {
b: 123,
c: '文字内容',
d() { return 2; }
};
Object.getOwnPropertyDescriptors(a);
// 输出结果
{
b:{
configurable: true,
enumerable: true,
value: 123,
writable: true,
[[Prototype]]: Object,
}
c:{
configurable: true,
enumerable: true,
value: "文字内容",
writable: true,
[[Prototype]]: Object,
}
d:{
configurable: true,
enumerable: true,
value: ƒ d(),
writable: true,
[[Prototype]]: Object,
},
[[Prototype]]: Object,
}
复制代码
4、String.prototype.padStart 和 String.prototype.padEnd
概述
padStart()
方法用另一个字符串填充当前字符串(如果需要的话,会重复多次),以便产生的字符串达到给定的长度。从当前字符串的开头(左侧)开始填充。
padEnd()
方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。该提案目前处于 Stage 4 阶段。
语法
str.padStart(targetLength [, padString])
targetLength
:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。padString
:可选的。填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断。此参数的默认值为" "
(U+0020)。str.padEnd(targetLength [, padString])
targetLength
:当前字符串需要填充到的目标长度。如果这个数值小于当前字符串的长度,则返回当前字符串本身。padString
:可选的。填充字符串。如果字符串太长,使填充后的字符串长度超过了目标长度,则只保留最左侧的部分,其他部分会被截断。此参数的缺省值为" "
(U+0020)。
例子:
'abc'.padStart(10); // " abc"
'abc'.padStart(10, "foo"); // "foofoofabc"
'abc'.padStart(6,"123465"); // "123abc"
'abc'.padStart(8, "0"); // "00000abc"
'abc'.padStart(1); // "abc"
复制代码
'abc'.padEnd(10); // "abc "
'abc'.padEnd(10, "foo"); // "abcfoofoof"
'abc'.padEnd(6, "123456"); // "abc123"
'abc'.padEnd(1); // "abc"
复制代码
ES9(ES2018)
1、for await…of
概述
for await...of
语句创建一个循环,该循环遍历异步可迭代对象以及同步可迭代对象,包括: 内置的 String、Array、类似数组对象(例如 arguments 或 NodeList)、TypedArray、Map、Set 和用户定义的异步/同步迭代器。它使用对象的每个不同属性的值调用要执行的语句来调用自定义迭代钩子。类似于
await
运算符一样,该语句只能在一个async function
内部使用。
动机
迭代器接口(在 ECMAScript 2015 中引入)是一种顺序数据访问协议,它支持开发通用和可组合的数据使用者和转换器。 由于在迭代器方法返回时必须知道序列中的下一个值和数据源的“完成”状态,因此迭代器仅适用于表示同步数据源。虽然 JavaScript 程序员遇到的许多数据源是同步的(例如内存列表和其他数据结构),但许多其他数据源不是。例如,任何需要 I/O 访问的数据源通常会使用基于事件或流式异步 API 来表示。不幸的是,迭代器不能用于表示这样的数据源。
(即使是 promise 的迭代器也是不够的,因为它只允许异步确定值,但需要同步确定“完成”状态。)
为了为异步数据源提供通用的数据访问协议,因此引入了异步迭代语句 for await...of
。
语法
async function process(iterable) { for await (variable of iterable) { // Do something. } }
variable
:在每次迭代中,将不同属性的值分配给变量。变量有可能以 const、let 或者 var 来声明。iterable
:被迭代枚举其属性的对象。与 for…of 相比,这里的对象可以返回 Promise,如果是这样,那么 variable 将是 Promise 所包含的值,否则是值本身。
例子:
function getTime(seconds){
return new Promise(resolve=>{
setTimeout(() => {
resolve(seconds)
}, seconds);
})
}
async function test(){
let arr = [getTime(500), getTime(300), getTime(3000)];
for await (let x of arr){
console.log(x); // 500 300 3000 按顺序的
}
}
test()
复制代码
2、Promise.prototype.finally
概述
finally()
方法返回一个 Promise。在 Promise 结束时,无论结果是 fulfilled 还是 rejected,都会执行指定的回调函数。这为在 Promise 是否成功完成后都需要执行的代码提供了一种方式。这避免了同样的语句需要在
then()
和catch()
中各写一次的情况。
动机
有些时候在 Promise 完成时,不管结果是 fulfilled
还是 rejected
,我们想执行一些清理工作,或是设置一些变量,这个是时候,我们不得不在 resolve
和 catch
中把同样的代码书写两遍,这是重复且无意义的工作。 Promise.finally()
提案,可以在 Promise 完成时,无论结果是 fulfilled
还是 rejected
,都会执行 finally
函数。
语法
Promise.resolve().then().catch(e => e).finally(onFinally);
onFinally
:Promise 结束后调用的 Function。
例子:
let isLoading = true;
fetch(myRequest).then(function(response) {
const contentType = response.headers.get("content-type");
if(contentType && contentType.includes("application/json")) {
return response.json();
}
throw new TypeError("Oops, we haven't got JSON!");
})
.then(function(json) { /* process your JSON further */ })
.catch(function(error) { console.log(error); })
.finally(function() { isLoading = false; });
复制代码
注意事项
finally()
虽然与 .then(onFinally, onFinally)
类似,但它们不同的是:
- 调用内联函数时,不需要多次声明该函数或为该函数创建一个变量保存它。
- 由于无法知道 promise 的最终状态,所以 finally 的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
- 与
Promise.resolve(2).then(() => {}, () => {})
(resolved 的结果为 undefined)不同,Promise.resolve(2).finally(() => {})
resolved 的结果为 2。 - 同样,
Promise.reject(3).then(() => {}, () => {})
(fulfilled 的结果为 undefined),Promise.reject(3).finally(() => {})
rejected 的结果为 3。
3、Object Rest/Spread Properties
概述
将
Rest
语法添加到解构中。Rest
属性收集那些尚未被解构模式拾取的剩余可枚举属性键。将
Spread
语法添加到对象字面量中。将已有对象的所有可枚举(enumerable
)属性拷贝到新构造的对象中。该提案目前处于 Stage 4 阶段。
动机
浅拷贝(Shallow-cloning,不包含 prototype)和对象合并,可以使用更简短的 Spread
语法。而不必再使用 Object.assign()
方式。
const obj1 = { foo: 'bar', x: 42 };
const obj2 = { foo: 'baz', y: 13 };
const clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }
const mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }
复制代码
有些时候我们在解构一个对象时,可能只关心它的其中一两个属性,这个时候 Rest
语法可以极大简化变量提取过程。
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
复制代码
语法
Rest
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
复制代码
Spread
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }
复制代码
注意事项
Object.assign()
函数会触发 setters
,而 Spread
语法则不会。
ES10(ES2019)
1、Array.prototype.flat 和 Array.prototype.flatMap
概述
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
flatMap()
方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
flatMap
方法与map
方法结合深度depth
为1
的flat
几乎相同。该提案目前处于 Stage 4 阶段。
动机
如果我们想扁平化一个多层嵌套的数组,代码如下:
// 使用 reduce、concat 和递归展开无限多层嵌套的数组
const arr = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
function flatDeep(arr, d = 1) {
return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), []) : arr.slice();
};
flatDeep(arr, Infinity);
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码
flat
提案正是用来简化这个流程:
const arr = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
arr.flat(Infinity);
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
复制代码
如果我们想将包含几句话的数组拆分成单个词组成的新数组,代码如下:
const arr = ["it's Sunny in", "", "California"];
const arr1 = arr.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]];
arr1.flat(2);
// ["it's", "Sunny", "in", "", "California"]
复制代码
flatMap
提案可以用来简化该流程:
const arr = ["it's Sunny in", "", "California"];
arr.flatMap(x => x.split(" "));
// ["it's", "Sunny", "in", "", "California"]
复制代码
语法
arr.flat([depth])
depth
:可选的。指定要提取嵌套数组的结构深度,默认值为1
。
例子:
[1, 2, [3, 4]].flat(1);
// [1, 2, 3, 4]
[1, 2, [3, 4, [5, 6]]].flat(2);
// [1, 2, 3, 4, 5, 6]
[1, 2, [3, 4, [5, 6]]].flat(Infinity);
// [1, 2, 3, 4, 5, 6]
复制代码
arr.flatMap( function callback( currentValue[, index[, array]]) { // return element for new_array }[, thisArg])
callback
:可以生成一个新数组中的元素的函数,可以传入三个参数:
currentValue
:当前正在数组中处理的元素。index
:可选的。数组中正在处理的当前元素的索引。array
:可选的。数组中正在处理的当前元素的索引。thisArg
:可选的。执行 callback 函数时 使用的this
值。
例子:
[1, 2, 3, 4].flatMap(a => [a ** 2]);
// [1, 4, 9, 16]
复制代码
map()
与 flatMap()
const arr1 = [1, 2, 3, 4];
arr1.map(x => [x * 2]);
// [[2], [4], [6], [8]]
arr1.flatMap(x => [x * 2]);
// [2, 4, 6, 8]
arr1.map(x => [x * 2]).flat(1);
// [2, 4, 6, 8]
// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]
复制代码
let arr1 = ["it's Sunny in", "", "California"];
arr1.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]
arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in", "", "California"]
复制代码
2、Object.fromEntries
概述
Object.fromEntries()
方法把键值对列表转换为一个对象。
Object.fromEntries()
执行与Object.entries
互逆的操作。该提案目前处于 Stage 4 阶段。
动机
const obj = { foo: true, bar: false };
const map = new Map(Object.entries(obj));
复制代码
如果要将类似上面代码中的 map
再转换为 obj
,我们不得不编写帮助函数:
const obj = Array.from(map).reduce((acc, [key, val]) => Object.assign(acc, { [key]: val }), {});
复制代码
Object.fromEntries
提案正是用来简化这个流程。
语法
Object.fromEntries(iterable);
iterable
:类似 Array、Map 或者其它实现了可迭代协议的可迭代对象。
例子:
嵌套数组
const obj = Object.fromEntries([['a', 0], ['b', 1]]);
// { a: 0, b: 1 }
复制代码
Object.entries
const obj = { abc: 1, def: 2, ghij: 3 };
const res = Object.fromEntries(
Object.entries(obj)
.filter(([key, val]) => key.length === 3)
.map(([key, val]) => [key, val * 2])
);
// { abc: 2, def: 4 }
复制代码
通过 Object.fromEntries
,可以将 Map
转化为 Object
:
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const obj = Object.fromEntries(map);
// { a: 1, b: 2, c: 3 }
复制代码
3、String.prototype.trimStart 和 String.prototype.trimEnd
概述
去除字符串首尾的连续空白符。
trimStart()
方法从字符串的开头删除空白符。trimLeft()
是此方法的别名。
trimEnd()
方法从字符串的结尾删除空白符。trimRight()
是此方法的别名。该提案目前处于 Stage 4 阶段。
动机
ES5 标准化了 String.prototype.trim
。 所有主要搜索引擎也实现了相应的 trimLeft
和 trimRight
功能,但是无任何标准规范。 为了与 padStart/padEnd
保持一致,所以提议使用 trimStart
和 trimEnd
。 考虑到不同浏览器的兼容性问题,trimLeft/trimRight
将作为 trimStart/trimEnd
的别名使用。
String.prototype.trimLeft === String.prototype.trimStart;
// true
String.prototype.trimRight === String.prototype.trimEnd;
// true
复制代码
语法
const greeting = ' Hello world! ';
console.log(greeting.trimStart());
// "Hello world! ";
console.log(greeting.trimEnd());
// " Hello world!";
复制代码
4、Optional Catch Binding
概述
该提案对 ECMAScript 进行了语法更改,允许在不使用 catch 绑定的情况下省略 catch 绑定。
该提案目前处于 Stage 4 阶段。
动机
try {
// try to use a web feature which may not be implemented
} catch (unused) {
// fall back to a less desirable web feature with broader support
}
复制代码
let isTheFeatureImplemented = false;
try {
// stress the required bits of the web API
isTheFeatureImplemented = true;
} catch (unused) {}
复制代码
普遍认为,声明或写入从未读取的变量会引发程序错误。
语法
try {
// ...
} catch {
// ...
}
复制代码
5、Symbol.prototype.description
概述
Symbol
对象可以通过一个可选的描述创建,可用于调试,但不能用于访问symbol
本身。
Symbol.prototype.description
属性可以用于读取该描述。与Symbol.prototype.toString()
不同的是它不会包含"Symbol()"
的字符串。
description
是一个只读属性,它会返回Symbol
对象的可选描述的字符串。该提案目前处于 Stage 4 阶段。
动机
直接暴露 symbol
的 description
内部插槽,取代通过 symbol.prototype.toString
间接暴露。
语法
Symbol('myDescription').description;
Symbol.iterator.description;
Symbol.for('foo').description;
复制代码
例子:
Symbol('desc').toString(); // "Symbol(desc)"
Symbol('desc').description; // "desc"
Symbol('').description; // ""
Symbol().description; // undefined
// well-known symbols
Symbol.iterator.toString(); // "Symbol(Symbol.iterator)"
Symbol.iterator.description; // "Symbol.iterator"
// global symbols
Symbol.for('foo').toString(); // "Symbol(foo)"
Symbol.for('foo').description; // "foo"
复制代码
ES11(ES2020)
1、String.prototype.matchAll
概述
matchAll()
方法返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器。
动机
之前,如果我们要获取所有匹配项信息,只能通过循环调用 regexp.exec()
( regexp 需使用 /g
标志):
const regexp = RegExp('foo[a-z]*', 'g');
const str = 'table football, foosball';
let match;
while ((match = regexp.exec(str)) !== null) {
console.log(`Found ${match[0]} start = ${match.index} end = ${regexp.lastIndex}.`);
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."
}
复制代码
如果使用 matchAll
,就可以不必使用 while
循环加 exec
方式(且正则表达式需使用 /g
标志)。使用 matchAll
会得到一个迭代器的返回值,配合 for…of、array spread 或者 Array.from() 可以更方便实现功能。
语法
str.matchAll(regexp)
regexp
:正则表达式对象。如果所传参数不是一个正则表达式对象,则会隐式地使用new RegExp(obj)
将其转换为一个RegExp
RegExp
必须是设置了全局模式 /g
的形式,否则会抛出异常 TypeError
。
例子:
const regexp = RegExp('foo[a-z]*', 'g');
const str = 'table football, foosball';
const matches = str.matchAll(regexp);
for (const match of matches) {
console.log(`Found ${match[0]} start = ${match.index} end = ${match.index + match[0].length}.`);
}
// expected output: "Found football start=6 end=14."
// expected output: "Found foosball start=16 end=24."
// matches iterator is exhausted after the for..of iteration
// Call matchAll again to create a new iterator
Array.from(str.matchAll(regexp), m => m[0]);
// Array [ "football", "foosball" ]
复制代码
注意事项
1、如果没有 /g
标志,matchAll
会抛出异常。
const regexp = RegExp('[a-c]', '');
const str = 'abc';
Array.from(str.matchAll(regexp), m => m[0]);
// TypeError: String.prototype.matchAll called with a non-global RegExp argument
复制代码
2、matchAll
内部做了一个 regexp
的复制,所以不像 regexp.exec
,lastIndex
在字符串扫描时不会改变。
const regexp = RegExp('[a-c]', 'g');
regexp.lastIndex = 1;
const str = 'abc';
Array.from(str.matchAll(regexp), m => `${regexp.lastIndex} ${m[0]}`);
// Array [ "1 b", "1 c" ]
复制代码
3、matchAll
的另外一个亮点是更好地获取捕获组。因为当使用 match()
和 /g
标志方式获取匹配信息时,捕获组会被忽略:
const regexp = /t(e)(st(/d?))/g;
const str = 'test1test2';
str.match(regexp);
// Array ['test1', 'test2']
复制代码
使用 matchAll
可以通过如下方式获取分组捕获:
let array = [...str.matchAll(regexp)];
array[0];
// ['test1', 'e', 'st1', '1', index: 0, input: 'test1test2', length: 4]
array[1];
// ['test2', 'e', 'st2', '2', index: 5, input: 'test1test2', length: 4]
复制代码
2、Dynamic Import
概述
动态导入模块
该提案目前处于 Stage 4 阶段。
动机
标准用法的 import
导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景:
- 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
- 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
- 当被导入的模块,在加载时并不存在,需要异步获取。
- 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)。
- 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)。
语法
import()
关键字 import
可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise
。
例子:
import('/modules/my-module.js')
.then((module) => {
// Do something with the module.
});
复制代码
也支持 await
关键字
const module = await import('/modules/my-module.js');
复制代码
模板字符串形式
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();
import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}
复制代码
注意事项
请不要滥用动态导入(只有在必要情况下采用)。 静态框架能更好地初始化依赖,而且更有利于静态分析工具和 tree shaking
发挥作用。
与静态 import
差异:
import()
可以从脚本中使用,而不仅仅是从模块中使用。- 如果
import()
在模块中使用,它可以出现在任何级别的任何地方,并且不会被提升。 import()
接受任意字符串(此处显示了运行时确定的模板字符串),而不仅仅是静态字符串文字。- 模块中存在
import()
并不会建立依赖关系,在对包含的模块求值之前,必须先获取并求值依赖关系。 import()
不建立可以静态分析的依赖关系(但是,在诸如import('./foo.js')
这样的简单情况下,实现可能仍然能够执行推测性抓取)。
3、import.meta
概述
import.meta
是一个给JavaScript
模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的URL
。该提案目前处于 Stage 4 阶段。
动机
通常情况下,主机环境可以提供有用的模块特定信息,以便在模块内进行代码评估。以下是几个例子。
模块的 URL 或 文件名
这是在 Node.js 的 CommonJS 模块系统中通过作用域内 __filename
变量(及其对应的 __dirname
)提供的。这允许通过代码轻松解析相对于模块文件的资源,例如:
const fs = require("fs");
const path = require("path");
const bytes = fs.readFileSync(path.resolve(__dirname, "data.bin"));
复制代码
如果没有可用的 _dirname
,像 fs.readFileSync('data.bin')
这样的代码将解析相对于当前工作目录的 data.bin
。这对于库作者来说通常是没有用的,因为他们将自己的资源与模块捆绑在一起,这些模块可以位于与 CWD 相关的任何位置。
浏览器中也存在相似的行为,将文件名替换为 URL。
scrpit 标签引入资源
<script data-option="value" src="library.js"></script>
// 获取 data-option 的值如下
const theOption = document.currentScript.dataset.option;
复制代码
为此使用(有效的)全局变量而不是词法范围的值的机制是有问题的,因为这意味着该值仅设置在顶级,并且必须保存在那里才能用于任何异步代码。
mainModule
在 Node.js 中,通常的做法是通过以下代码来确定您是程序的 main
模块还是 entry
模块:
if (module === process.mainModule) {
// run tests for this library, or provide a CLI interface, or similar
}
复制代码
也就是说,它允许单个文件在导入时用作库,或者在直接运行时用作副作用程序。
请注意,此特定公式如何依赖于将主机提供的作用域值模块与主机提供的全局值 process.mainModule
进行比较。
其他可设想的情况
- 其他
Node.js
方法,如module.children
或require.resolve()
- 关于模块所属
package
的信息,或者通过Node.js
的package.json
或web package
- 指向嵌入在
HTML
模块中的JavaScript
模块的包含DocumentFragment
的指针
语法
import.meta
import.meta
对象由一个关键字 import
,一个点符号
和一个 meta
属性名组成。通常情况下 import.
是作为一个属性访问的上下文,但是在这里 import
不是一个真正的对象。
import.meta
对象带有一个 null
的原型对象。这个对象可以扩展,并且它的属性都是可写,可配置和可枚举的。
例子:
以下代码使用了我们添加到 import.meta
中的两个属性:
(async () => {
const response = await fetch(new URL("../hamsters.jpg", import.meta.url));
const blob = await response.blob();
const size = import.meta.scriptElement.dataset.size || 300;
const image = new Image();
image.src = URL.createObjectURL(blob);
image.width = image.height = size;
document.body.appendChild(image);
})();
复制代码
当这个模块被加载时,无论它的位置如何,它都会加载 hamsters.jpg
文件,并显示图像。可以使用用于导入它的脚本元素来配置图像的大小,例如:
<script type="module" src="path/to/hamster-displayer.mjs" data-size="500"></script>
复制代码
4、BigInt
概述
在 JavaScript 中,
BigInt
是一种数字类型的数据,它可以表示任意精度格式的整数。(新的基本数据类型)该提案目前处于 Stage 4 阶段。
动机
在 JavaScript 中,当 number
大于 Number.MAX_SAFE_INTEGER
时,就会发生精度丢失。
const x = Number.MAX_SAFE_INTEGER;
// 9007199254740991, this is 1 less than 2^53
const y = x + 1;
// 9007199254740992, ok, checks out
const z = x + 2;
// 9007199254740992, wait, that’s the same as above!
复制代码
BigInt
的提案正是要解决这个问题,BigInt
使 Number.MAX_SAFE_INTEGER
不再是 JavaScript 的限制。它可以表示任意精度的整数。
语法
通过
BigInt
方法,或是在一个数值后添加n
后缀,来将一个 Number 转换为 BigInt 类型。
例子:
const theBiggestInt = 9007199254740991n;
const alsoHuge = BigInt(9007199254740991);
// 9007199254740991n
const hugeButString = BigInt('9007199254740991');
// 9007199254740991n
复制代码
Number.MAX_SAFE_INTEGER;
// 9007199254740991
Number.MAX_SAFE_INTEGER + 10 - 10;
// 9007199254740990 (精度丢失)
BigInt(Number.MAX_SAFE_INTEGER) + 10n - 10n;
// 9007199254740991n (计算结果为 bigint 类型)
typeof 9007199254740991n === "bigint";
// true
复制代码
你可以使用 +
、*
、-
、**
、%
运算符,就像在 Number 中使用一样。
const previousMaxSafe = BigInt(Number.MAX_SAFE_INTEGER);
// 9007199254740991
const maxPlusOne = previousMaxSafe + 1n;
// 9007199254740992n
const theFuture = previousMaxSafe + 2n;
// 9007199254740993n, this works now!
const multi = previousMaxSafe * 2n;
// 18014398509481982n
const subtr = multi – 10n;
// 18014398509481972n
const mod = multi % 10n;
// 2n
const bigN = 2n ** 54n;
// 18014398509481984n
bigN * -1n
// –18014398509481984n
复制代码
注意事项
1、/
除法运算符也可以使用,但是会向下取整。
因为 BigInt
不是 BigDecimal
。
const expected = 4n / 2n; // 2n
const rounded = 5n / 2n; // 2n, not 2.5n
复制代码
2、BigInt
并不严格等于 Number
。
2n === 2; // false
2n == 2; // true
复制代码
3、比较运算符可以正常使用。
1n < 2; // true
2n > 1; // true
2 > 2; // false
2n > 2; // false
2n >= 2; // true
复制代码
4、数组中混合 BigInt
和 Number
可以正常排序。
const mixed = [4n, 6, -12n, 10, 4, 0, 0n];
// [4n, 6, -12n, 10, 4, 0, 0n]
mixed.sort();
// [-12n, 0, 0n, 10, 4n, 4, 6]
复制代码
5、BigInt
不可以与 Number
混合运算。
1n + 2;
// TypeError: Cannot mix BigInt and other types, use explicit conversions
1n * 2;
// TypeError: Cannot mix BigInt and other types, use explicit conversions
复制代码
5、Promise.allSettled
概述
Promise.allSettled()
方法返回一个在所有给定的promise
都已经fulfilled
或rejected
后的promise
,并带有一个对象数组,每个对象表示对应的promise
结果。当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个
promise
的结果时,通常使用它。相比之下,
Promise.all()
更适合彼此相互依赖或者在其中任何一个reject
时立即结束。该提案目前处于 Stage 4 阶段。
动机
api | 描述 | |
---|---|---|
Promise.allSettled | 不会短路 | 本提案 ? |
Promise.all | 当存在输入值 rejected 时短路 | 在 ES2015 中添加 ✅ |
Promise.race | 当存在输入值 稳定 时短路 | 在 ES2015 中添加 ✅ |
Promise.any | 当存在输入值 fulfilled 时短路 | 在 ES2021 中添加 ✅ |
可以看出,Promise.all
、Promise.race
、Promise.any
都存在短路行为。
Promise.allSettled
不存在短路行为,它确保每个 promise
都可以执行完毕,无论状态是 fulfilled
还是 rejected
。
语法
Promise.allSettled(iterable)
iterable
:一个可迭代的对象,例如Array
,其中每个成员都是Promise
。
一旦所指定的 promises
集合中每一个 promise
已经完成,无论是成功的达成或被拒绝,未决议的 promise
将被异步完成。那时,所返回的 promise
的处理器将传入一个数组作为输入,该数组包含原始 promises
集中每个 promise
的结果。
对于每个结果对象,都有一个 status
字符串。如果它的值为 fulfilled
,则结果对象上存在一个 value
。如果值为 rejected
,则存在一个 reason
。value
(或 reason
)反映了每个 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"
复制代码
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => reject('我是失败的Promise_1'));
const promise4 = new Promise((resolve, reject) => reject('我是失败的Promise_2'));
const promiseList = [promise1, promise2, promise3, promise4];
Promise.allSettled(promiseList)
.then(values => {
console.log(values);
});
复制代码
6、globalThis
概述
全局属性
globalThis
包含全局的this
值,类似于全局对象(global object)。该提案目前处于 Stage 4 阶段。
动机
编写访问全局对象的可移植 ECMAScript 代码是很困难的。在 web
端,可以是 window
或者 self
或者 this
或者 frames
。在 node.js
中,是 global
或者 this
。
其中,只有 this
在 V8 的 d8
或者 JavaScriptCore 的 jsc
这样的 shell
中可用。 在 sloppy
模式下的独立函数调用中,this
也可以工作,但是在模块中或在函数内的严格模式下它是 undefined
。
在这样的上下文中,全局对象仍然可以通过 Function('return this')()
访问,但在某些 CSP 设置中,例如在 Chrome 应用程序中是不可访问的。
下面是一些从外部获取全局对象的代码,作为单个参数传递给 IIFE,它在大多数情况下都有效,但实际上不适用于 d8
当在模块中或在函数内的严格模式下(可以使用函数技巧修复):
function foo() {
// If we're in a browser, the global namespace is named 'window'. If we're
// in node, it's named 'global'. If we're in a shell, 'this' might work.
(typeof window !== "undefined"
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
}
复制代码
此外,由于 CSP 问题,es6-shim 必须从 Function('return this')()
转换, 当前处理 browsers
、node
、web workers
、frames
方式如下:
const getGlobal = function () {
// the only reliable means to get the global object is
// `Function('return this')()`
// However, this causes CSP violations in Chrome apps.
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
复制代码
语法
在浏览器中, window
、globalThis
、this
三者全等:
window === this; // true
window === globalThis; // true
this === globalThis; // true
复制代码
在 node.js 中,global
、globalThis
、this
三者全等:
> global === this; // true
> global === globalThis; // true
> this === globalThis; // true
复制代码
7、Nullish Coalescing
概述
空值合并操作符(
??
)是一个逻辑操作符,当左侧的操作数为null
或者undefined
时,返回其右侧操作数,否则返回左侧操作数。该提案目前处于 Stage 4 阶段。
动机
在执行属性访问时,如果属性访问的结果为 null
或 undefined
,则通常需要提供默认值。目前,在 JavaScript 中表达此意图的一种典型方式是使用 ||
运算符。
const response = {
settings: {
nullValue: null,
height: 400,
animationDuration: 0,
headerText: '',
showSplashScreen: false,
},
};
const undefinedValue = response.settings.undefinedValue || 'some other default';
// result: 'some other default'
const nullValue = response.settings.nullValue || 'some other default';
// result: 'some other default'
复制代码
这对于 null
和 undefined
的值常见情况非常有效,但有许多 falsy
值可能会产生令人惊讶的结果:
const headerText = response.settings.headerText || 'Hello, world!';
// Potentially unintended. '' is falsy, result: 'Hello, world!'
const animationDuration = response.settings.animationDuration || 300;
// Potentially unintended. 0 is falsy, result: 300
const showSplashScreen = response.settings.showSplashScreen || true;
// Potentially unintended. false is falsy, result: true
复制代码
语法
leftExpr ?? rightExpr
例子:
let user = {
u1: 0,
u2: false,
u3: null,
u4: undefined,
u5: '',
}
let u2 = user.u2 ?? '用户2'; // false
let u3 = user.u3 ?? '用户3'; // 用户3
let u4 = user.u4 ?? '用户4'; // 用户4
let u5 = user.u5 ?? '用户5'; // ''
复制代码
8、Optional Chaining
概述
可选链操作符(
?.
)允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?.
操作符的功能类似于.
链式操作符,不同之处在于,在引用(null
或者undefined
)的情况下不会引起错误,该表达式短路返回值是undefined
。与函数调用一起使用时,如果给定的函数不存在,则返回undefined
。该提案目前处于 Stage 4 阶段。
动机
在寻找对象结构深处的属性值时,通常必须检查中间节点是否存在:
const street = user.address && user.address.street;
复制代码
此外,许多 API
返回对象或 null/undefined
,并且可能希望仅在结果不为 null
时从结果中提取属性:
const fooInput = myForm.querySelector('input[name=foo]');
const fooValue = fooInput ? fooInput.value : undefined;
复制代码
语法
obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)
例子:
使用可选链访问对象属性:
let user = {};
let u1 = user.childer.name;
// TypeError: Cannot read property 'name' of undefined
let u1 = user?.childer?.name;
// undefined
复制代码
使用可选链进行函数调用
function doSomething(onContent, onError) {
try {
// ... do something with the data
}
catch (err) {
onError?.(err.message); // 如果onError是undefined也不会有异常
}
}
复制代码
当使用方括号与属性名的形式来访问属性时,你也可以使用可选链操作符:
const nestedProp = obj?.['prop' + 'Name'];
复制代码
可选链访问数组元素:
let arrayItem = arr?.[42];
复制代码
ES12(ES2021)
1、String.prototype.replaceAll
概述
replaceAll()
方法返回一个新字符串,新字符串所有满足pattern
的部分都已被replacement
替换。pattern
可以是一个字符串或一个RegExp
,replacement
可以是一个字符串或一个在每次匹配被调用的函数。该提案目前处于 Stage 4 阶段。
动机
目前没有办法在不使用全局正则表达式的情况下替换字符串中子字符串的所有实例。 String.prototype.replace
仅在与字符串参数一起使用时替换掉第一次出现的子字符串。
目前实现这一目标的最常见方法是使用全局正则表达式:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replace(/+/g, ' ');
复制代码
这种方法的缺点是需要对特殊的 RegExp
字符进行转义——注意转义的 +
。
另一种解决方案是结合 String.split
和 Array.join
:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.split('+').join(' ');
复制代码
这种方法避免了任何转义,但带来了将字符串拆分为数组,然后再将数组各项重新粘合在一起的开销。
语法
String.prototype.replaceAll(searchValue, replaceValue);
返回一个全新的字符串,所有符合匹配规则的字符都将被替换掉。
除了以下两种情况外,在其他所有情况下 String.prototype.replaceAll
行为都与 String.prototype.replace
相同:
- 如果
searchValue
是一个字符串,String.prototype.replace
只替换一次出现的searchValue
,而String.prototype.replaceAll
替换所有出现的searchValue
。 - 如果
searchValue
是非全局正则表达式,则String.prototype.replace
替换单个匹配项,而String.prototype.replaceAll
抛出异常。这样做是为了避免缺少全局标志(这意味着“不要全部替换”)和被调用方法的名称(强烈建议“全部替换”)之间的固有混淆。
例子:
const queryString = 'q=query+string+parameters';
const withSpaces = queryString.replaceAll('+', ' ');
复制代码
2、Promise.any
概述
Promise.any()
接收一个Promise
可迭代对象,只要其中的一个promise
成功,就返回那个已经成功的promise
。如果可迭代对象中没有一个promise
成功(即所有的promises
都失败/拒绝),就返回一个失败的promise
。该提案目前处于 Stage 4 阶段。
语法
Promise.any(iterable)
- iterable:一个可迭代的对象,例如 Array。
例子:
const promise1 = new Promise((resolve, reject) => reject('失败的Promise1'));
const promise2 = new Promise((resolve, reject) => reject('失败的Promise2'));
const promiseList = [promise1, promise2];
Promise.any(promiseList)
.then(values=>{
console.log(values);
})
.catch(error=>{
console.log(error);
});
复制代码
在上面的示例中,error
是 AggregateError
,它是一个新的 error
子类,将各个错误组合在一起。每个 AggregateError
实例都包含一个指向异常数组的指针。
3、Logical Assignment Operators
概述
逻辑赋值运算符
该提案目前处于 Stage 4 阶段。
语法
||=
&&=
??=
例子:
a ||= b;
//等价于
a = a || (a = b);
a &&= b;
//等价于
a = a && (a = b);
a ??= b;
//等价于
a = a ?? (a = b);
复制代码
4、Numeric Separators
概述
数字分隔符
它是将其早期草案与
Christophe Porteneuve
的提案numeric underscores
合并的结果,以扩展现有的NumericLiteral
以允许数字之间的分隔符。该提案目前处于 Stage 4 阶段。
动机
此功能使开发人员能够通过在数字之间创建视觉分隔来使他们的数字文字更具可读性。
语法
使用下划线
_
(U+005F)在数字间进行分隔。
例子:
十进制
const price1 = 1_000_000_000;
const price2 = 1000000000;
price1 === price2; // true
const float1 = 0.000_001;
const float2 = 0.000001;
float1 === float2; // true;
复制代码
二进制
const nibbles1 = 0b1010_0001_1000_0101;
const nibbles2 = 0b1010000110000101;
nibbles1 === nibbles2; // true
复制代码
十六进制
const message1 = 0xA1_B1_C1;
const message2 = 0xA1B1C1;
message1 === message2; // true
复制代码
BigInt
const big1 = 1_000_000_000n;
const big2 = 1000000000n;
big1 === big2; // true
复制代码
{{m.name}}
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/156619.html