-->

详解 ES 2018 新特性(收藏)

2019-08-03 22:11发布



前言

第9版ECMAScript标准于2018年6月发布,正式名称为ECMAScript 2018(简称ES2018)。从ES2016开始,ECMAScript规范的新版本每年发布一次,而不是每隔几年发布一次,相应的,每版增加的功能也更少一些。最新版本的标准通过添加4个新的RegExp特性、rest/spread属性、异步迭代和Promise.prototype.finally来延续每年的发布周期。此外,ES2018取消了标记模板转义序列的语法限制。

以下将逐一解释这些变动:

一、Rest/Spread 特性

ES2015中添加的最有趣的特性之一是spread操作符。你可以用它替换concat()和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]

在数组必须以拆解的方式作为函数参数的情况下,spread操作符也很有用。例如:

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

ES2018通过向对象文本添加扩展属性进一步扩展了这种语法。他可以将一个对象的属性拷贝到另一个对象上,参考以下情形:

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

在上述代码中,spread操作符遍历obj1属性,并将其添加到obj2的属性中;而在之前的版本中,如此处理会抛出一个异常。需要注意的是,如果存在相同的属性名,只有最后一个会生效。

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属性;而spread操作忽略了setter。

划重点:spread只复制枚举属性。在下面的例子中,type属性不会出现在复制对象中,因为它的枚举属性被设置为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继承了car中的color属性。因为spread操作只会复制对象自身的属性,color并没有出现在新的对象中。

spread只会进行浅拷贝,如果属性的值是一个对象的话,只有对象的引用会被拷贝:

const
obj = {x: {y:
10
}};
const
copy1 = {...obj};
const
copy2 = {...obj};
console.log(copy1.x === copy2.x);
// → true

copy1.x 和 copy2.x 指向同一个对象的引用,所以他们严格相等。

ES2015增加的另一个有用特性是rest参数,它允许JS使用……将值表示为数组:

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属性将剩余的可枚举属性复制到一个新对象中。注意,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

Rest/Spread 特性支持


Node.js:

  • 8.0.0 (需要 --harmony 运行环境)
  • 8.3.0 (完全支持)

二、异步迭代

遍历是编程的一个重要部分。JS提供了for、for…in和while以及map()、filter()和forEach()等遍历数据的方法。在ES2015则引入了迭代器接口。

包含Symbol.iterator属性的对象是可迭代对象,如字符串和集合对象(如Set、Map和Array)。如下为迭代遍历的示例:

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为布尔值,表示遍历是否结束。

普通对象进行迭代需要定义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}

对象的迭代器通过Object.keys()方法获取属性名数组,将其赋值给values常量,同时定义一个默认值为0的计数器。当迭代器开始执行时,会返回一个包含next()方法的对象。该方法会返回包含value和done的对象,value为下一迭代值,done为布尔值,表示迭代器是否到达终点。

上述实现方式还是过于复杂,可以通过generator函数简化:

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}

在该generator函数中,利用for in循环枚举生成属性值。结果与前面的示例完全相同,但是要短得多。

迭代器的缺点是不适合表示异步数据源。ES2018的解决方案是异步迭代器和异步迭代。异步迭代器与传统迭代器的不同之处在于,它没有返回{value, done}形式的普通对象,而是返回一个Promise,其resolve返回{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}
}));

注意,promise+迭代器并不能代替异步迭代器。虽然一个普通的同步迭代器可以异步地确定值,但是它仍然需要同步地确定“完成”的状态。

当然,您同样可以使用generator函数简化该过程,如下所示:

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值则变为一个promise对象

在可迭代对象上迭代的一个简单方法是使用for of,但由于异步迭代对象的value和done并不是同步指定的,因此for of并不适用。基于此,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()方法可能返回一个包含rejects的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.

异步迭代器支持


Node.js:

  • 8.10.0 (需要--harmony async iteration标志)
  • 10.0.0 (全部支持)

三、Promise.prototype.finally

ES2018的另一个令人兴奋的新特性是finally()方法。几个JavaScript库以前实现过类似的方法,这在许多情况下都很有用。这鼓励Ecma技术委员会正式将finally()添加到规范中。无论promise的结果如何,finally()方法中的代码都会执行。让我们看一个简单的例子:

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()方法在请求数据之后隐藏loading,无论请求是否成功。

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

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()中可能出现的错误。

Promise.prototype.finall支持


Node.js: 10.0.0 (全部支持)

四、新的正则表达式特性

ES2018为正则表达式添加了四个新特性,进一步提高了JavaScript的字符串处理能力。这些特点如下:

  • s (dotAll) 标志
  • 命名捕获组
  • Lookbehind 后行断言
  • Unicode属性转义

s (dotAll) 标志

点(.)是正则表达式模式中的一个特殊字符,它匹配除换行符(如换行符(n)或回车符(r)之外的任何字符。匹配所有字符(包括换行符)的一种方法是使用一个包含两个短字符的字符类,比如[dD]。这个表达式查询数字(d)或非数字(D)字符。因此,它匹配任何字符:

console.log(
/one[dD]two/
.test(
'onentwo'
));
// → true

ES2018引入了一种模式,在这种模式中,点(.)可以用来实现相同的结果。通过在原正则表达式基础上添加s表示,可以激活该模式:

console.log(
/one.two/
.test(
'onentwo'
));
// → false
console.log(
/one.two/
s.test(
'onentwo'
));
// → true

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

命名捕获组

在一些正则表达式模式中,使用数字进行匹配可能会令人混淆。例如,使用正则表达式/(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引入了使用(?…)语法的命名捕获组。因此,匹配日期的模式可以用一种不那么模棱两可的方式来写:

const
re =
/(?d{4})-(?d{2})-(?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语法重复调用名称捕获组。例如,要在一个句子中找到连续重复的单词,可以使用/b(?w+)s+kb/:

const
re =
/b(?w+)s+kb/
;
const
match = re.
exec
(
'Get that that cat off the table!'
);
console.log(match.index);
// → 4
console.log(match[
0
]);
// → that that

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

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

Lookbehind后行断言

ES2018将lookbehind后行断言引入JavaScript,以前JavaScript只支持前行断言。后行断言由(?<=…)表示,代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配pattern。例如,如果您想匹配以美元、英镑或欧元表示的产品的价格,而不需要捕获货币符号,您可以使用/(?<=$|£|€)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]

还有一种负向后行断言,表示为(?

const
re =
/(?;
console.log(re.
exec
(
'We regret this service is currently unavailable'
));
// → null
console.log(re.
exec
(
'The service is available'
));
// → ["available", index: 15, input: "The service is available", groups: undefined]

Unicode 属性转义

ES2018提供了一种新的转义序列类型,称为Unicode属性转义,可以匹配所有的Unicode。你可以使用p{Number}来匹配所有的Unicode数字,例如,假设你想匹配的Unicode字符㉛字符串:

const
str =
'㉛'
;
console.log(
/d/
u.test(str));
// → false
console.log(
/p{Number}/
u.test(str));
// → true

同样的,你可以使用p{Alphabetic}来匹配所有的Unicode单词字符:

const
str =
'ض'
;
console.log(
/p{Alphabetic}/
u.test(str));
// → true
// the w shorthand cannot match ض
console.log(
/w/
u.test(str));
// → false

同样有一个负向的Unicode属性转义模板 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属性列表。

新正则表达式支持


Node.js:

  • 8.3.0 (需要 --harmony 标志)
  • 8.10.0 (支持 s (dotAll) 标志和后行断言)
  • 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、unicode转义的u和八进制转义的u。因此,像“C:xxxuuu”或“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:xxxuuu;

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

const
result = ubuntu;
// → SyntaxError: Invalid Unicode escape sequence

模板文字修订支持


Node.js:

  • 8.3.0 (需要 --harmony 标志)
  • 8.10.0 (全部支持)

总结

我们已经很好地了解了ES2018中引入的几个关键特性,包括异步迭代、rest/spread属性、Promise.prototype.finally()以及正则表达式新特性的添加。尽管一些浏览器厂商还没有完全实现其中的一些特性,但是仍然可以用诸如Babel之类转义器进行

如果这篇文章对你有所帮助,请多多点赞收藏转发评论,让更多的人看到这篇文章。不要忘记点关注,后续我还会分享更多精彩有趣的文章。

如果您正在学习编程,可以私信我学习免费领取学习资料。

文章来源: https://www.toutiao.com/group/6720746123973100040/