每个JavaScript开发人员都应该知道的新ES2018功能(译文)

前言

正文

ECMAScript标准的第九版,官方称为ECMAScript 2018(或简称ES2018),于2018年6月发布。从ES2016开始,ECMAScript规范的新版本每年发布而不是每几年发布一次,并且添加的功能少于主要版本以前。该标准的最新版本通过添加四个新RegExp功能,rest/spread属性,asynchronous iteration,和Promise.prototype.finally。此外,ES2018从标记模板中删除了转义序列的语法限制。

这些新变化将在后面的小节中解释。

rest/spread属性

ES2015最有趣的功能之一是点差运算符。该运算符使复制和合并数组变得更加简单。您可以使用运算符...,而不是调用concat()or slice()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
const arr1 = [10, 20, 30];

// make a copy of arr1
const copy = [...arr1];

console.log(copy); // → [10, 20, 30]

const arr2 = [40, 50];

// merge arr2 with arr1
const merge = [...arr1, ...arr2];

console.log(merge); // → [10, 20, 30, 40, 50]

在必须作为函数的单独参数传入数组的情况下,扩展运算符也派上用场。例如:

1
2
3
4
5
const arr = [10, 20, 30]

// equivalent to
// console.log(Math.max(10, 20, 30));
console.log(Math.max(...arr)); // → 30

ES2018通过向对象文字添加扩展属性来进一步扩展此语法。使用spread属性,您可以将对象的自身可枚举属性复制到新对象上。请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
const obj1 = {
a: 10,
b: 20
};

const obj2 = {
...obj1,
c: 30
};

console.log(obj2); // → {a: 10, b: 20, c: 30}

在此代码中,...运算符用于检索属性obj1并将其分配给obj2。在ES2018之前,尝试这样做会引发错误。如果有多个具有相同名称的属性,则将使用最后一个属性:

1
2
3
4
5
6
7
8
9
10
11
const obj1 = {
a: 10,
b: 20
};

const obj2 = {
...obj1,
a: 30
};

console.log(obj2); // → {a: 30, b: 20}

Spread属性还提供了一种合并两个或多个对象的新方法,可以将其用作方法的替代Object.assign()方法:

1
2
3
4
5
6
7
8
9
const obj1 = {a: 10};
const obj2 = {b: 20};
const obj3 = {c: 30};

// ES2018
console.log({...obj1, ...obj2, ...obj3}); // → {a: 10, b: 20, c: 30}

// ES2015
console.log(Object.assign({}, obj1, obj2, obj3)); // → {a: 10, b: 20, c: 30}

但请注意,spread属性并不总是产生相同的结果Object.assign()。请考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.defineProperty(Object.prototype, 'a', {
set(value) {
console.log('set called!');
}
});

const obj = {a: 10};

console.log({...obj});
// → {a: 10}

console.log(Object.assign({}, obj));
// → set called!
// → {}

在此代码中,该Object.assign()方法执行继承的setter属性。相反,传播属性完全忽略了setter。

重要的是要记住,spread属性只复制可枚举的属性。在以下示例中,type属性不会显示在复制的对象中,因为其enumerable属性设置为false

1
2
3
4
5
6
7
8
9
10
const car = {
color: 'blue'
};

Object.defineProperty(car, 'type', {
value: 'coupe',
enumerable: false
});

console.log({...car}); // → {color: "blue"}

即使它们是可枚举的,也会忽略继承的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const car = {
color: 'blue'
};

const car2 = Object.create(car, {
type: {
value: 'coupe',
enumerable: true,
}
});

console.log(car2.color); // → blue
console.log(car2.hasOwnProperty('color')); // → false

console.log(car2.type); // → coupe
console.log(car2.hasOwnProperty('type')); // → true

console.log({...car2}); // → {type: "coupe"}

在此代码中,car2继承color属性car。因为spread属性只复制对象的自己的属性,color所以不包含在返回值中。

请记住,spread属性只能生成对象的浅表副本。如果属性包含对象,则仅复制对象的引用:

1
2
3
4
5
const obj = {x: {y: 10}};
const copy1 = {...obj};
const copy2 = {...obj};

console.log(copy1.x === copy2.x); // → true

这里copy1copy2的x是指在内存中的同一对象,所以全等运算返回true

ES2015中添加的另一个有用功能是rest参数,它使JavaScript程序员可以使用它...来表示值作为数组。例如:

1
2
3
4
5
const arr = [10, 20, 30];
const [x, ...rest] = arr;

console.log(x); // → 10
console.log(rest); // → [20, 30]

这里,arr的第一个值被分配给对应的x,而剩余的元素被分配给rest变量。这种称为阵列解构的模式变得如此受欢迎,以至于Ecma技术委员会决定为对象带来类似的功能:

1
2
3
4
5
6
7
8
9
10
const obj = {
a: 10,
b: 20,
c: 30
};

const {a, ...rest} = obj;

console.log(a); // → 10
console.log(rest); // → {b: 20, c: 30}

此代码使用解构赋值中的其余属性将剩余的自身可枚举属性复制到新对象中。请注意,rest属性必须始终出现在对象的末尾,否则会引发错误:

1
2
3
4
5
6
7
const obj = {
a: 10,
b: 20,
c: 30
};

const {...rest, a} = obj; // → SyntaxError: Rest element must be last element

还要记住,在对象中使用多个rest会导致错误,除非它们是嵌套的:

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
a: 10,
b: {
x: 20,
y: 30,
z: 40
}
};

const {b: {x, ...rest1}, ...rest2} = obj; // no error

const {...rest, ...rest2} = obj; // → SyntaxError: Rest element must be last element

Support for Rest/Spread

Chrome Firefox Safari Edge
60 55 11.1 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
60 55 11.3 No 8.2 60

Node.js

  • 8.0.0(运行时需要加-harmony
  • 8.3.0(完全支持)

Asynchronous Iteration(异步迭代)

迭代数据集是编程的重要部分。此前ES2015,提供的JavaScript语句如forfor...inwhile,和方法map()filter()以及forEach()都用于此目的。为了使程序员能够一次一个地处理集合中的元素,ES2015引入了迭代器接口。

如果对象具有Symbol.iterator属性,则该对象是可迭代的。在ES2015中,字符串和集合对象(如Set,Map和Array)带有Symbol.iterator属性,因此可以迭代。以下代码给出了如何一次访问可迭代元素的示例:

1
2
3
4
5
6
7
const arr = [10, 20, 30];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}

Symbol.iterator是一个众所周知的符号,指定一个返回迭代器的函数。与迭代器交互的主要方法是next()方法。此方法返回具有两个属性的对象:valuedonevalue属性为集合中下一个元素的值。done属性的值为truefalse表示集合是否迭代完成。

默认情况下,普通对象不可迭代,但如果在其上定义Symbol.iterator属性,则它可以变为可迭代,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.iterator]() {
const values = Object.keys(this);
let i = 0;
return {
next: () => {
return {
value: this[values[i++]],
done: i > values.length
}
}
};
}
};

const iterator = collection[Symbol.iterator]();

console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}

此对象是可迭代的,因为它定义了一个Symbol.iterator属性。迭代器使用该Object.keys()方法获取对象属性名称的数组,然后将其分配给values常量。它还定义了一个计数器变量i,并给它一个初始值0.当执行迭代器时,它返回一个包含next()方法的对象。每次调用next()方法时,它都返回一对{value, done}value保持集合中的下一个元素并done保持一个布尔值,指示迭代器是否已达到集合的需要。

虽然这段代码完美无缺,但却不必要。使用生成器函数可以大大简化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.iterator]: function * () {
for (let key in this) {
yield this[key];
}
}
};

const iterator = collection[Symbol.iterator]();

console.log(iterator.next()); // → {value: 10, done: false}
console.log(iterator.next()); // → {value: 20, done: false}
console.log(iterator.next()); // → {value: 30, done: false}
console.log(iterator.next()); // → {value: undefined, done: true}

在这个生成器中,for...in循环用于枚举集合并产生每个属性的值。结果与前一个示例完全相同,但它大大缩短了。

迭代器的缺点是它们不适合表示异步数据源。ES2018的补救解决方案是异步迭代器和异步迭代。异步迭代器与传统迭代器的不同之处在于,它不是以形式返回普通对象{value, done},而是返回履行的承诺{value, done}。异步迭代定义了一个返回异步迭代器的Symbol.asyncIterator方法(而不是Symbol.iterator)。

一个例子让这个更清楚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]() {
const values = Object.keys(this);
let i = 0;
return {
next: () => {
return Promise.resolve({
value: this[values[i++]],
done: i > values.length
});
}
};
}
};

const iterator = collection[Symbol.asyncIterator]();

console.log(iterator.next().then(result => {
console.log(result); // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: 20, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: 30, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: undefined, done: true}
}));

请注意,不可使用promises的迭代器来实现相同的结果。虽然普通的同步迭代器可以异步确定值,但它仍然需要同步确定done的状态。

同样,您可以使用生成器函数简化过程,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]: async function * () {
for (let key in this) {
yield this[key];
}
}
};

const iterator = collection[Symbol.asyncIterator]();

console.log(iterator.next().then(result => {
console.log(result); // → {value: 10, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: 20, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: 30, done: false}
}));

console.log(iterator.next().then(result => {
console.log(result); // → {value: undefined, done: true}
}));

通常,生成器函数返回带有next()方法的生成器对象。当调用next()时,它返回一个{value,done},其value属性保存了yield值。异步生成器执行相同的操作,除了它返回一个履行{value,done}的promise。

迭代可迭代对象的一种简单方法是使用for...of语句,但是for...of不能与async iterables一起使用,因为valuedone不是同步确定的。因此,ES2018提供了for...await...of。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const collection = {
a: 10,
b: 20,
c: 30,
[Symbol.asyncIterator]: async function * () {
for (let key in this) {
yield this[key];
}
}
};

(async function () {
for await (const x of collection) {
console.log(x);
}
})();

// logs:
// → 10
// → 20
// → 30

在此代码中,for...await...of语句隐式调用Symbol.asyncIterator集合对象上的方法以获取异步迭代器。每次循环时,都会调用迭代器的next()方法,它返回一个promise。一旦解析了promise,就会将结果对象的value属性读取到x变量中。循环继续,直到返回的对象的done属性值为true

请记住,该for...await...of语句仅在异步生成器和异步函数中有效。违反此规则会导致一个SyntaxError报错。

next()方法可能会返回拒绝的promise。要优雅地处理被拒绝的promise,您可以将for...await...of语句包装在语句中try...catch,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const collection = {
[Symbol.asyncIterator]() {
return {
next: () => {
return Promise.reject(new Error('Something went wrong.'))
}
};
}
};

(async function() {
try {
for await (const value of collection) {}
} catch (error) {
console.log('Caught: ' + error.message);
}
})();

// logs:
// → Caught: Something went wrong.

Support for Asynchronous Iterators

Chrome Firefox Safari Edge
63 57 12 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 57 12 No 8.2 63

Node.js

  • 8.0.0(运行时需要加-harmony
  • 8.3.0(完全支持)

Promise.prototype.finally

ES2018的另一个令人兴奋的补充是finally()方法。一些JavaScript库之前已经实现了类似的方法,这在许多情况下证明是有用的。这鼓励了Ecma技术委员会正式添加finally()到规范中。使用这个方法,程序员将能不管promise的结果如何,都能执行一个代码块。我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
fetch('https://www.google.com')
.then((response) => {
console.log(response.status);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
document.querySelector('#spinner').style.display = 'none';
});

finally()无论操作是否成功,当您需要在操作完成后进行一些清理时,该方法会派上用场。在此代码中,该finally()方法只是在获取和处理数据后隐藏加载微调器。代码不是在then()catch()方法中复制最终逻辑,而是在promise被fulfilled或rejected后注册要执行的函数。

你可以使用promise.then(func,func)而不是promise.finally(func)来实现相同的结果,但你必须在fulfillment处理程序和rejection处理程序中重复相同的代码,或者为它声明一个变量:

1
2
3
4
5
6
7
8
9
10
11
12
fetch('https://www.google.com')
.then((response) => {
console.log(response.status);
})
.catch((error) => {
console.log(error);
})
.then(final, final);

function final() {
document.querySelector('#spinner').style.display = 'none';
}

then()catch()一样,finally()方法总是返回一个promise,因此可以链接更多的方法。通常,您希望使用finally()作为最后一个链,但在某些情况下,例如在发出HTTP请求时,最好链接另一个catch()以处理finally()中可能发生的错误。

Support for Promise.prototype.finally

Chrome Firefox Safari Edge
63 58 11.1 18
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
63 58 11.1 No 8.2 63

Node.js

  • 10.0.0(完全支持)

新的RegExp功能

ES2018为该RegExp对象增加了四个新功能,进一步提高了JavaScript的字符串处理能力。这些功能如下:

  • S(DOTALL)标志
  • Named Capture Groups(命名捕获组)
  • Lookbehind Assertions(后向断言)
  • Unicode Property Escapes(Unicode属性转义)

S(DOTALL)标志

点(.)是正则表达式模式中的特殊字符,它匹配除换行符之外的任何字符,例如换行符(\n)或回车符(\r)。匹配所有字符(包括换行符)的解决方法是使用具有两个相反短字的字符类,例如[\d\D]。此字符类告诉正则表达式引擎找到一个数字(\d)或非数字(\D)的字符。因此,它匹配任何字符:

1
console.log(/one[\d\D]two/.test('one\ntwo'));    // → true

ES2018引入了一种模式,其中点可用于实现相同的结果。可以使用s标志在每个正则表达式的基础上激活此模式:

1
2
console.log(/one.two/.test('one\ntwo'));     // → false
console.log(/one.two/s.test('one\ntwo')); // → true

使用标志来选择新行为的好处是向后兼容性。因此,使用点字符的现有正则表达式模式不受影响。

Named Capture Groups(命名捕获组)

在一些正则表达式模式中,使用数字来引用捕获组可能会令人困惑。例如,使用/(\d{4})-(\d{2})-(\d{2})/与日期匹配的正则表达式。由于美式英语中的日期符号与英式英语不同,因此很难知道哪个组指的是哪一天,哪个组指的是月份:

1
2
3
4
5
6
7
const re = /(\d{4})-(\d{2})-(\d{2})/;
const match= re.exec('2019-01-10');

console.log(match[0]); // → 2019-01-10
console.log(match[1]); // → 2019
console.log(match[2]); // → 01
console.log(match[3]); // → 10

ES2018引入了使用(?<name>...)语法的命名捕获组。因此,匹配日期的模式可以用不那么模糊的方式编写:

1
2
3
4
5
6
7
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = re.exec('2019-01-10');

console.log(match.groups); // → {year: "2019", month: "01", day: "10"}
console.log(match.groups.year); // → 2019
console.log(match.groups.month); // → 01
console.log(match.groups.day); // → 10

您可以使用\k<name>语法在模式中稍后调用命名的捕获组。例如,要在句子中查找连续的重复单词,您可以使用/\b(?<dup>\w+)\s+\k<dup>\b/

1
2
3
4
5
const re = /\b(?<dup>\w+)\s+\k<dup>\b/;
const match = re.exec('Get that that cat off the table!');

console.log(match.index); // → 4
console.log(match[0]); // → that that

要将命名的捕获组插入到方法的替换字符串中replace(),您需要使用$<name>构造。例如:

1
2
3
4
5
6
7
const str = 'red & blue';

console.log(str.replace(/(red) & (blue)/, '$2 & $1'));
// → blue & red

console.log(str.replace(/(?<red>red) & (?<blue>blue)/, '$<blue> & $<red>'));
// → blue & red

Lookbehind Assertions(后向断言)

ES2018为JavaScript带来了后向性断言,这些断言已经在其他正则表达式实现中可用多年。以前,JavaScript只支持超前断言。后向断言用表示(?<=...),并使您能够匹配基于模式之前的子字符串的模式。例如,如果要在不捕获货币符号的情况下以美元,英镑或欧元匹配产品的价格,则可以使用/(?<=\$|£|€)\d+(\.\d*)?/

1
2
3
4
5
6
7
8
9
10
const re = /(?<=\$|£|€)\d+(\.\d*)?/;

console.log(re.exec('199'));
// → null

console.log(re.exec('$199'));
// → ["199", undefined, index: 1, input: "$199", groups: undefined]

console.log(re.exec('€50'));
// → ["50", undefined, index: 1, input: "€50", groups: undefined]

还有一个lookbehind的否定版本,用(?<!...),只有当模式前面没有lookbehind中的模式时,负lookbehind才允许您匹配模式。例如,模式/(?<!un)available/匹配没有“un”前缀的可用词

这段翻译的不好,放上原文

There is also a negative version of lookbehind, which is denoted by (?<!...). A negative lookbehind allows you to match a pattern only if it is not preceded by the pattern within the lookbehind. For example, the pattern /(?<!un)available/ matches the word available if it does not have a “un” prefix:

Unicode Property Escapes(Unicode属性转义)

ES2018提供了一种称为Unicode属性转义的新类型转义序列,它在正则表达式中提供对完整Unicode的支持。假设您要在字符串中匹配Unicode字符㉛。虽然㉛被认为是一个数字,但是你不能将它与\d速记字符类匹配,因为它只支持ASCII [0-9]字符。另一方面,Unicode属性转义可用于匹配Unicode中的任何十进制数:

1
2
3
4
const str = '㉛';

console.log(/\d/u.test(str)); // → false
console.log(/\p{Number}/u.test(str)); // → true

同样,如果要匹配任何Unicode字母字符,你可以使用\p{Alphabetic}

1
2
3
4
5
6
const str = 'ض';

console.log(/\p{Alphabetic}/u.test(str)); // → true

// the \w shorthand cannot match ض
console.log(/\w/u.test(str)); // → false

还有一个否定版本\p{...},表示为\P{...}

1
2
3
4
5
console.log(/\P{Number}/u.test('㉛'));    // → false
console.log(/\P{Number}/u.test('ض')); // → true

console.log(/\P{Alphabetic}/u.test('㉛')); // → true
console.log(/\P{Alphabetic}/u.test('ض')); // → false

除了字母和数字之外,还有几个属性可以在Unicode属性转义中使用。您可以在当前规范提案中找到支持的Unicode属性列表。

Support for New RegExp

* Chrome Firefox Safari Edge
S(DOTALL)标志 62 No 11.1 No
命名捕获组 64 No 11.1 No
后向断言 62 No No No
Unicode属性转义 64 No 11.1 No
* Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
S(DOTALL)标志 62 No 11.3 No 8.2 62
命名捕获组 64 No 11.3 No No 64
后向断言 62 No No No 8.2 62
Unicode属性转义 64 No 11.3 No No 64

Node.js

  • 8.3.0 (运行时需要加-harmony)
  • 8.10.0 (support for s (dotAll) flag and lookbehind assertions)
  • 10.0.0 (完全支持)

模板字符串

当模板字符串紧跟在表达式之后时,它被称为标记模板字符串。当您想要使用函数解析模板文字时,标记的模板会派上用场。请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
function fn(string, substitute) {
if(substitute === 'ES6') {
substitute = 'ES2015'
}
return substitute + string[1];
}

const version = 'ES6';
const result = fn`${version} was a major update`;

console.log(result); // → ES2015 was a major update

在此代码中,调用标记表达式(它是常规函数)并传递模板文字。该函数只是修改字符串的动态部分并返回它。

在ES2018之前,标记的模板字符串具有与转义序列相关的语法限制。反斜杠后跟某些字符序列被视为特殊字符:\x解释为十六进制转义符,\u解释为unicode转义符,\后跟一个数字解释为八进制转义符。其结果是,字符串,例如”C:\xxx\uuu“或者”\ubuntu“被认为是由解释无效转义序列,并会抛出SyntaxError。

ES2018从标记模板中删除了这些限制,而不是抛出错误,表示无效的转义序列如下undefined

1
2
3
4
5
6
7
function fn(string, substitute) {
console.log(substitute); // → escape sequences:
console.log(string[1]); // → undefined
}

const str = 'escape sequences:';
const result = fn`${str} \ubuntu C:\xxx\uuu`;

请记住,在常规模板文字中使用非法转义序列仍会导致错误:

1
2
const result = `\ubuntu`;
// → SyntaxError: Invalid Unicode escape sequence

Support for Template Literal Revision

Chrome Firefox Safari Edge
62 56 11 No
Chrome Android Firefox Android iOS Safari Edge Mobile Samsung Internet Android Webview
62 56 11 No 8.2 62

Node.js

  • 8.3.0 (运行时需要加-harmony
  • 8.10.0(完全支持)

总结

我们已经仔细研究了ES2018中引入的几个关键特性,包括异步迭代,rest/spread属性Promise.prototype.finally()以及RegExp对象的添加。虽然其中一些浏览器供应商尚未完全实现其中一些功能,但由于像Babel这样的JavaScript转换器,它们今天仍然可以使用。

ECMAScript正在迅速发展,并且每隔一段时间就会引入新功能,因此请查看已完成提案的列表,了解新功能的全部内容。

第一次翻译文章,能力有限,水平一般,翻译不妥之处,还望指正。感谢。

Copyright © 2017 - 2024 Timbok's Blog All Rights Reserved.

访客数 : | 访问量 :