跳到主要内容

ECMAScript 2018 新特性

1. 异步遍历 ( for await of)

当我们需要控制异步队列的顺序时,往往很难实现,而异步迭代机制让这种操作变得简单。

在async/await的某些时刻, 你可能尝试在同步循环中调用异步函数。例如:

async function process(array) {
for (let i of array) {
await doSomething(i);
}
}

这段代码不会正常运行,下面这段同样也不会:

async function process(array) {
array.forEach(async i => {
await doSomething(i);
});
}

这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调用完成。

异步迭代语句: for-await-of, 它是 for-of语句的变体, 可以对异步可迭代对象进行迭代, 例如:

async function process(array) {
for await (let i of array) {
doSomething(i);
}
}

2. Promise.prototype.finally

一个Promise调用链要么成功到达最后一个.then(),要么失败触发.catch()

在某些情况下, 你想要在无论Promise运行成功还是失败, 运行相同的代码, 例如清除, 删除对话, 关闭数据库连接等

.finally() 允许你指定最终的逻辑:

function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
// finish here!
});
}

和 java 中的 finally 一样, Promise.finally 一定会被执行:

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

和 java 一样,我们可以在 finally 中做一些资源清理的工作:

let connection;
db.open()
.then(conn => {
connection = conn;
return connection.select({ name: 'Jane' });
})
.then(result => {
...
})
···
.catch(error => {
// handle errors
})
.finally(() => {
connection.close();
});

上面的例子中,我们开启了一个数据库的连接,在使用完之后,我们在 finally 中对其进行 close 操作


3. Rest / Spread 操作符和对象构建

RestSpread 的操作符都是 ..., 只不过使用的场景和目的不一样

  • Rest 主要用在 对象的解构, 目前只支持对象的解构 和 不确定的参数描述

  • Spread 主要用在 字面量对象的构建上

下面我们分别来介绍:

3.1 Rest

如果用在对象的解构中, 除了已经手动指定的属性名之外, Rest 将会拷贝对象其他的所有可枚举 (enumerable)的属性

const obj = {foo: 1, bar: 2, baz: 3};
const {foo, ...rest} = obj;
// Same as:
// const foo = 1;
// const rest = {bar: 2, baz: 3};

如果用在参数中, Rest 表示的是所有剩下的参数:

function func({param1, param2, ...rest}) { // rest operator
console.log('All parameters: ',
{param1, param2, ...rest}); // spread operator
return param1 + param2;
}

注意,在 Obj 字面量中, Rest 运算符只能放在 Obj 的最顶层,并且只能使用一次,还要放在最后

const {...rest, foo} = obj; // SyntaxError
const {foo, ...rest1, ...rest2} = obj; // SyntaxError

当然你还可以嵌套使用 Rest 运算符:

const obj = {
foo: {
a: 1,
b: 2,
c: 3,
},
bar: 4,
baz: 5,
};
const {foo: {a, ...rest1}, ...rest2} = obj;
// Same as:
// const a = 1;
// const rest1 = {b: 2, c: 3};
// const rest2 = {bar: 4, baz: 5};

3.2 Spread

Spread 主要被用来展开对象, 能够被展开对象的属性一定要是可枚举的 enumerable

> const obj = {foo: 1, bar: 2};
> {...obj, baz: 3}
{ foo: 1, bar: 2, baz: 3 }

如果对象的属性 key 一样, 那么后面属性值会覆盖之前的属性值

> const obj = {foo: 1, bar: 2, baz: 3};
> {...obj, foo: true}
{ foo: true, bar: 2, baz: 3 }
> {foo: true, ...obj}
{ foo: 1, bar: 2, baz: 3 }
信息

创建和拷贝对象

使用 Object.assignSpread 操作符可以很方便的进行对象的拷贝

我们看一个最简单的对象拷贝的例子:

const clone1 = {...obj};
const clone2 = Object.assign({}, obj);

但是这样的拷贝有个缺点,就是只能拷贝自有的可枚举的属性

并且拷贝之后对象的 prototypes 是 Object.prototype, 也就是说没有继承被拷贝对象的 prototype

> Object.getPrototypeOf(clone1) === Object.prototype
true
> Object.getPrototypeOf(clone2) === Object.prototype
true
> Object.getPrototypeOf({}) === Object.prototype
true

如果想要同时拷贝对象的 prototype, 则可以这样做:

const clone1 = {__proto__: Object.getPrototypeOf(obj), ...obj};
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)), obj);

或者指定对象内置的 proto 属性,或者从 obj 的 prtotype 创建一个新的对象

注意,对象内置的 proto 属性只在部分浏览器中支持

Object.assignSpread 只能拷贝可枚举的属性,如果是 set, get 属性或者想要拷贝属性的 attributes (writable, enumerable),那么就需要用到我们之前讲到的 Object.getOwnPropertyDescriptors

const clone1 = Object.defineProperties({},
Object.getOwnPropertyDescriptors(obj));

const clone2 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
~~

> 注意,我们使用的所有的拷贝都是浅拷贝。如果被拷贝的对象内部又有对象的话,拷贝的只是这个对象的引用

~~~js
const original = { prop: {} };
const clone = Object.assign({}, original);

console.log(original.prop === clone.prop); // true
original.prop.foo = 'abc';
console.log(clone.prop.foo); // abc
信息

SpreadObject.assign() 的区别

assgin() 在拷贝对象的时候,会调用相应属性的 set 方法,而 Spread 不会

举个例子, 我们先给 Object.prototype 定义一个 set 方法:

Object.defineProperty(Object.prototype, 'foo', {
set(value) {
console.log('SET', value);
},
});
const obj = {foo: 123};

然后看一下拷贝的区别:

> Object.assign({}, obj)
SET 123
{}

> { ...obj }
{ foo: 123 }

可以看到 assign() 会触发 set 方法,而 Spread 不会

另外, 如果对象属性是不可写的, 那么 assign() 将会报错,而 Spread 不会

我们先定义一个不可写的对象:

Object.defineProperty(Object.prototype, 'bar', {
writable: false,
value: 'abc',
});

看下赋值操作:

> const tmp = {};
> tmp.bar = 123;
TypeError: Cannot assign to read only property 'bar'

> Object.assign({}, obj)
TypeError: Cannot assign to read only property 'bar'

> { ...obj }
{ bar: 123 }

4. 模板文字 和 带标签的模板文字

模板文字和带标签的模板文字是在ES6中引入的, 在ES9中进行了修正

我们先看下什么是模本文字, 模板文字 (Template literals) 就是在反引号中输入的文字, 在其中可以使用 ${···})来进行变量的解析, 并且还支持回车换行

const firstName = 'Jane';
console.log(`Hello ${firstName}!
How are you
today?`);

// Output:
// Hello Jane!
// How are you
// today?

而带标签的模板文字是指在模板文字之前放上一个函数调用:

String.raw`\u{4B}`
'\u{4B}'

这里 String.raw 被称为 tag function, 我们看下 raw 的定义:

raw(template: TemplateStringsArray, ...substitutions: any[]): string;

上面的代码还可以改写为:

String.raw`\u004B`
'\u004B'

\u{4B}\u004B 都是字符 K 的 unicode 表示

上面的 raw 其实可以这样表示

function tagFunc(tmplObj, substs) {
return {
Cooked: tmplObj,
Raw: tmplObj.raw,
};
}

我们可以这样使用:

> tagFunc`\u{4B}`;
{ Cooked: [ 'K' ], Raw: [ '\\u{4B}' ] }

5. 正则表达式支持 unicode 属性转义

到目前为止,在正则表达式中本地访问 Unicode 字符属性是不被允许的

ES2018添加了 Unicode 属性转义——形式为 \p{...}\P{...}

在正则表达式中使用标记 u (unicode) 设置,在\p块儿内, 可以以键值对的方式设置需要匹配的属性而非具体内容。例如:

const reGreekSymbol = /\p{Script=Greek}/u;
reGreekSymbol.test('π'); // true

此特性可以避免使用特定 Unicode 区间来进行内容类型判断,提升可读性和可维护性


6. 正则表达式 反向断言(lookbehind)

目前JavaScript在正则表达式中支持先行断言 (lookahead)

这意味着匹配会发生,但不会有任何捕获,并且断言没有包含在整个匹配字段中。例如从价格中捕获货币符号:

const reLookahead = /\D(?=\d+)/,
match = reLookahead.exec('$123.89');
console.log( match[0] ); // $

ES2018引入以相同方式工作但是匹配前面的反向断言 (lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字:

const reLookbehind = /(?<=\D)\d+/,
match = reLookbehind.exec('$123.89');
console.log( match[0] ); // 123.89

7. 正则表达式命名组

JavaScript正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组, 例如: 以 YYYY-MM-DD 的格式解析日期:

const
reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match[1], // 2018
month = match[2], // 04
day = match[3]; // 30

这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引

ES2018允许命名捕获组使用符号 ?, 在打开捕获括号 (后立即命名,示例如下:

const
reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match.groups.year, // 2018
month = match.groups.month, // 04
day = match.groups.day; // 30

任何匹配失败的命名组都将返回 undefined

命名捕获也可以使用在 replace() 方法中

例如将日期转换为美国的 MM-DD-YYYY 格式:

const
reDate = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/,
d = '2018-04-30',
usDate = d.replace(reDate, '$<month>-$<day>-$<year>');

8. 正则表达式 dotAll模式

dotAll 是一个新的正则表达式修饰符,目前 JS 拥有的修饰符有:

  • g -> global
  • i -> ingoreCase
  • m -> multiline
  • y -> sticky
  • u -> unicode
  • s -> dotAll

正则表达式中点.匹配除回车外的任何单字符, 标记 s 改变这种行为, 允许行终止符的出现, 例如:

/hello.world/.test('hello\nworld');  // false
/hello.world/s.test('hello\nworld'); // true

正常情况下dot . 代表的是一个字符, 但是这个字符不能够代表行的结束符

> /^.$/.test('\n')
false

而dotAll是在 dot . 匹配后面引入的 s, 它可以被用来匹配行的结束符:

> /^.$/s.test('\n')
true

在ES中, 有下面几种字符表示的都是行的结束符:

  • U+000A LINE FEED (LF) (\n) - 换行
  • U+000D CARRIAGE RETURN (CR) (\r) - 回车
  • U+2028 LINE SEPARATOR - 行分隔符
  • U+2029 PARAGRAPH SEPARATOR - 段分隔符

还有一些其它字符, 也可以作为一行的开始:

  • U+000B VERTICAL TAB (\v)
  • U+000C FORM FEED (\f)
  • U+0085 NEXT LINE

目前 . 只能匹配其中的一部分:

let regex = /./

regex.test('\n') // false
regex.test('\r') // false
regex.test('\u{2028}') // false
regex.test('\u{2029}') // false

regex.test('\v') // true
regex.test('\f') // true
regex.test('\u{0085}') // true

标记 s 表示 dotAll, 用来改变 . 不能匹配行终止符的行为:

/hello.world/.test('hello\nworld')  // false
/hello.world/s.test('hello\nworld') // true

或者用 \s 来匹配空白符:

/hello.world/.test('hello\nworld')  // false
/hello[\s]world/s.test('hello\nworld') // true

dotAll 表示 . 可以匹配任意字符:

const re = /hello.world/s  // 等价于 const re = new RegExp('hello.world', 's')

re.test('hello\nworld') // true
re.dotAll // true
re.flags // 's'