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

43次阅读

共计 13667 个字符,预计需要花费 35 分钟才能阅读完成。

前言

原文地址:https://css-tricks.com/new-es2018-features-every-javascript-developer-should-know/

原文作者:Faraz Kelhini

译者:Timbok

翻译工具:Google Translate

本文首发于我的个人网站: Timbok.top

正文
ECMAScript 标准的第九版,官方称为 ECMAScript 2018(或简称 ES2018),于 2018 年 6 月发布。从 ES2016 开始,ECMAScript 规范的新版本每年发布而不是每几年发布一次,并且添加的功能少于主要版本以前。该标准的最新版本通过添加四个新 RegExp 功能,rest/spread 属性,asynchronous iteration,和 Promise.prototype.finally。此外,ES2018 从标记模板中删除了转义序列的语法限制。
这些新变化将在后面的小节中解释。
rest/spread 属性
ES2015 最有趣的功能之一是点差运算符。该运算符使复制和合并数组变得更加简单。您可以使用运算符 …,而不是调用 concat()or slice()方法:
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]
在必须作为函数的单独参数传入数组的情况下,扩展运算符也派上用场。例如:
const arr = [10, 20, 30]

// equivalent to
// console.log(Math.max(10, 20, 30));
console.log(Math.max(…arr)); // → 30
ES2018 通过向对象文字添加扩展属性来进一步扩展此语法。使用 spread 属性,您可以将对象的自身可枚举属性复制到新对象上。请考虑以下示例:
const obj1 = {
a: 10,
b: 20
};

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

console.log(obj2); // → {a: 10, b: 20, c: 30}
在此代码中,… 运算符用于检索属性 obj1 并将其分配给 obj2。在 ES2018 之前,尝试这样做会引发错误。如果有多个具有相同名称的属性,则将使用最后一个属性:
const obj1 = {
a: 10,
b: 20
};

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

console.log(obj2); // → {a: 30, b: 20}
Spread 属性还提供了一种合并两个或多个对象的新方法,可以将其用作方法的替代 Object.assign()方法:
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()。请考虑以下代码:
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:
const car = {
color: ‘blue’
};

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

console.log({…car}); // → {color: “blue”}
即使它们是可枚举的,也会忽略继承的属性:
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 属性只能生成对象的浅表副本。如果属性包含对象,则仅复制对象的引用:
const obj = {x: {y: 10}};
const copy1 = {…obj};
const copy2 = {…obj};

console.log(copy1.x === copy2.x); // → true
这里 copy1 和 copy2 的 x 是指在内存中的同一对象,所以全等运算返回 true。
ES2015 中添加的另一个有用功能是 rest 参数,它使 JavaScript 程序员可以使用它 … 来表示值作为数组。例如:
const arr = [10, 20, 30];
const [x, …rest] = arr;

console.log(x); // → 10
console.log(rest); // → [20, 30]
这里,arr 的第一个值被分配给对应的 x,而剩余的元素被分配给 rest 变量。这种称为阵列解构的模式变得如此受欢迎,以至于 Ecma 技术委员会决定为对象带来类似的功能:
const obj = {
a: 10,
b: 20,
c: 30
};

const {a, …rest} = obj;

console.log(a); // → 10
console.log(rest); // → {b: 20, c: 30}
此代码使用解构赋值中的其余属性将剩余的自身可枚举属性复制到新对象中。请注意,rest 属性必须始终出现在对象的末尾,否则会引发错误:
const obj = {
a: 10,
b: 20,
c: 30
};

const {…rest, a} = obj; // → SyntaxError: Rest element must be last element
还要记住,在对象中使用多个 rest 会导致错误,除非它们是嵌套的:
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 语句如 for,for…in 和 while,和方法 map(),filter()以及 forEach()都用于此目的。为了使程序员能够一次一个地处理集合中的元素,ES2015 引入了迭代器接口。
如果对象具有 Symbol.iterator 属性,则该对象是可迭代的。在 ES2015 中,字符串和集合对象(如 Set,Map 和 Array)带有 Symbol.iterator 属性,因此可以迭代。以下代码给出了如何一次访问可迭代元素的示例:
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()方法。此方法返回具有两个属性的对象:value 和 done。value 属性为集合中下一个元素的值。done 属性的值为 true 或 false 表示集合是否迭代完成。
默认情况下,普通对象不可迭代,但如果在其上定义 Symbol.iterator 属性,则它可以变为可迭代,如下例所示:
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 保持一个布尔值,指示迭代器是否已达到集合的需要。
虽然这段代码完美无缺,但却不必要。使用生成器函数可以大大简化过程:
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)。
一个例子让这个更清楚:
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 的状态。
同样,您可以使用生成器函数简化过程,如下所示:
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 一起使用,因为 value 和 done 不是同步确定的。因此,ES2018 提供了 for…await…of。我们来看一个例子:
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,如下所示:
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 的结果如何,都能执行一个代码块。我们来看一个简单的例子:
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 处理程序中重复相同的代码,或者为它声明一个变量:
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)的字符。因此,它匹配任何字符:
console.log(/one[\d\D]two/.test(‘one\ntwo’)); // → true
ES2018 引入了一种模式,其中点可用于实现相同的结果。可以使用 s 标志在每个正则表达式的基础上激活此模式:
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})/ 与日期匹配的正则表达式。由于美式英语中的日期符号与英式英语不同,因此很难知道哪个组指的是哪一天,哪个组指的是月份:
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>…) 语法的命名捕获组。因此,匹配日期的模式可以用不那么模糊的方式编写:
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/:
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> 构造。例如:
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*)?/:
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 中的任何十进制数:
const str = ‘ ㉛ ’;

console.log(/\d/u.test(str)); // → false
console.log(/\p{Number}/u.test(str)); // → true
同样,如果要匹配任何 Unicode 字母字符,你可以使用 \p{Alphabetic}:
const str = ‘ض’;

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

// the \w shorthand cannot match ض
console.log(/\w/u.test(str)); // → false
还有一个否定版本 \p{…},表示为 \P{…}:
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 (完全支持)

模板字符串
当模板字符串紧跟在表达式之后时,它被称为标记模板字符串。当您想要使用函数解析模板文字时,标记的模板会派上用场。请考虑以下示例:
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
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`;
请记住,在常规模板文字中使用非法转义序列仍会导致错误:
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 正在迅速发展,并且每隔一段时间就会引入新功能,因此请查看已完成提案的列表,了解新功能的全部内容。
第一次翻译文章,能力有限,水平一般,翻译不妥之处,还望指正。感谢。

正文完
 0