一.概览
2个主特性:
正则表达式相关的4个小特性:
其它:
二.Asynchronous Iteration
普通的(同步)迭代是类似这样的:
let arr = [1, 2, 3];
let iter = arr[Symbol.iterator]();
// 手动遍历
while (true) {
let step = iter.next();
if (step.done) break;
console.log(step.value); // 1, 2, 3
}
或者通过for...of
循环去遍历:
for (let value of arr) {
console.log(value); // 1, 2, 3
}
但如果数据源是异步的,for...of
循环就只能拿到一堆Promise,而不是想要的值:
// 异步数据源
let arr = [1, 2, 3].map(n => Promise.resolve(n));
for (let value of arr) {
console.log(value); // Promise.{<resolved>: 1}...
}
这是因为ES2015推出的Iterator接口仅适用于同步数据源:
Iterators are only suitable for representing synchronous data sources
为了支持异步数据源,ES2018新增了3个东西:
异步迭代器接口:AsyncIterator
异步迭代语句:
for-await-of
异步(迭代器的)生成器:async generator functions
async generator用来快速实现AsyncIterator接口,而实现了AsyncIterator接口的东西就能够方便地通过for-await-of
遍历了
AsyncIterator
类似于同步Iterator接口:
const { value, done } = syncIterator.next();
异步AsyncIterator接口要求next()
返回携带着{ value, done }
的Promsie:
asyncIterator.next().then(({ value, done }) => /* ... */);
接口对应的方法名为Symbol.asyncIterator
,例如:
let myObj = {/* ... */};
// 实现了Symbol.asyncIterator就说明我是可被异步迭代的(async iterable)
myObj[Symbol.asyncIterator] = () => {
return {
next() {
return Promise.resolve({ value: "more and more...", done: false });
}
}
};
试玩:
let asyncIter = myObj[Symbol.asyncIterator]();
(async () => {
while (true) {
let step = await asyncIter.next();
if (step.done) break;
console.log(step.value); // more and more...死循环,无限序列嘛
}
})();
P.S.同步Iterator接口对应的方法名为Symbol.iterator
,具体见for…of循环_ES6笔记1 | 2.不能遍历对象
for-await-of
类似的,实现了AsyncIterator接口的,就叫async iterable,就有能通过for-await-of
遍历的特权:
// 异步数据源
let arr = [1, 2, 3].map(n => Promise.resolve(n));
// 实现AsyncIterator接口
arr[Symbol.asyncIterator] = () => {
let i = 0;
return {
next() {
let done = i === arr.length;
return !done ?
arr[i++].then(value => ({ value, done })) :
Promise.resolve({ value: void 0, done: true });
}
}
};
(async ()=> {
for await (const n of arr) {
console.log(n); // 1, 2, 3
}
})();
用起来与同步for...of
没太大区别,只是实现AsyncIterator接口有些麻烦,迫切需要一种更方便的方式
P.S.同样,await
关键字只能出现在async
function里,for-await-of
的await
也不例外
async generator
async generator就是我们迫切想要的异步迭代器的生成器:
// 异步数据源
let arr = [1, 2, 3].map(n => Promise.resolve(n));
// 实现AsyncIterator接口
arr[Symbol.asyncIterator] = async function*() {
for (let value of arr) {
yield value;
}
}
方便多了,更进一步地,async generator返回值本来就是async iterable(隐式实现了AsyncIterator接口),没必要手动实现该接口:
let asyncIterable = async function*() {
let arr = [1, 2, 3].map(n => Promise.resolve(n));
for (let value of arr) {
yield value;
}
}();
类似于同步版本:
let iterable = function*() {
let arr = [1, 2, 3];
for (let value of arr) {
yield value;
}
}();
就具体语法而言,async generator有3个特点:
返回async iterable对象,其
next
、throw
、return
方法都返回Promise,而不直接返回{ value, done }
,并且会默认实现Symbol.asyncIterator
方法(因此async generator返回async iterable)函数体中允许出现
await
、for-await-of
语句同样支持
yield\*
拼接迭代器
例如:
let asyncIterable = async function*() {
let arr = [1, 2, 3].map(n => Promise.resolve(n));
for (let value of arr) {
yield value;
}
// yield*拼接异步迭代器
yield* (async function*() {
for (let v of [4, 5, 6]) {
yield v;
}
}());
// 允许出现await
let seven = await Promise.resolve(7);
yield seven;
// 允许出现for-await-of
for await (let x of [8, 9]) {
yield x;
}
}();
// test
(async ()=> {
for await (const n of asyncIterable) {
console.log(n); // 1, 2, 3...9
}
})();
P.S.注意一个细节,类似于await nonPromise
,for-wait-of
也能接受非Promise值(同步值)
P.S.另外,async generator里的yield
等价于yield await
,具体见Suggestion: Make yield Promise.reject(...)
uncatchable
实现原理
Implicit in the concept of the async iterator is the concept of a request queue. Since iterator methods may be called many times before the result of a prior request is resolved, each method call must be queued internally until all previous request operations have completed.
asyncIterator内部维持了一个请求队列,以此保证遍历次序,例如:
const sleep = (ts) => new Promise((resolve) => setTimeout(resolve, ts));
let asyncIterable = async function*() {
yield sleep(3000);
yield sleep(1000);
}();
const now = Date.now();
const time = () => Date.now() - now;
asyncIterable.next().then(() => console.log('first then fired at ' + time()));
asyncIterable.next().then(() => console.log('second then fired at ' + time()));
输出:
first then fired at 3002
second then fired at 4005
第一个next()
结果还没完成,立即发起的第二个next()
,会被记到队列里,等到前置next()
都完成以后,才实际去做
上例相当于:
let iterable = function*() {
let first;
yield first = sleep(3000);
// 排队,等到前置yield promise都完成以后,才开始
yield first.then(() => sleep(1000));
}();
iterable.next().value.then(() => console.log('first then fired at ' + time()));
iterable.next().value.then(() => console.log('second then fired at ' + time()));
P.S.关于请求队列机制的更多信息,请查看ES2018: asynchronous iteration | await in async generators
三.Rest/Spread Properties
ES2015里推出了3种...
的语法:
例如:
// 不定参数
function f(first, second, ...rest) {
console.log(rest);
}
// 剩余元素
const iterable = [1, 2, 3, 4];
const [first, second, ...rest] = iterable;
// 展开元素
f(...iterable);
ES2018新增了两种:
剩余属性
基本用法如下:
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
z; // { a: 3, b: 4 }
嵌套结构同样适用:
let complex = {
x: { a: 1, b: 2, c: 3 }
};
let {
x: { a: xa, ...xbc }
} = complex;
常见的应用场景:
// 浅拷贝(不带原型属性)
let { ...aClone } = a;
// 扩展选项参数
function baseFunction({ a, b }) {
// ...
}
function wrapperFunction({ x, y, ...restConfig }) {
// do something with x and y
// pass the rest to the base function
return baseFunction(restConfig);
}
需要特别注意,解构赋值与剩余属性的差异:
let { x, y, ...z } = a;
// is not equivalent to
let { x, ...n } = a;
let { y, ...z } = n;
这两种方式看似等价,实则不然:
let a = Object.create({x: 1, y: 2});
a.z = 3;
void (() => {
let { x, y, ...z } = a;
console.log(x, y, z); // 1 2 {z: 3}
})();
void (() => {
let { x, ...n } = a;
let { y, ...z } = n;
console.log(x, y, z); // 1 undefined {z: 3}
})();
关键区别在于剩余属性只取自身属性,而解构赋值会取自身及原型链上的属性,所以对照组中的y
变成undefined
了(n
拿不到原型属性y
,仅拿到了实例属性z
)
展开属性
基本用法示例:
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }
常见应用场景:
// 浅拷贝(不带原型属性)
let aClone = { ...a };
// 等价于
let aClone = Object.assign({}, a);
// merge多个对象
let ab = { ...a, ...b };
// 等价于
let ab = Object.assign({}, a, b);
// 重写属性
let aWithOverrides = { ...a, x: 1, y: 2 };
// 或者
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等价于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
// 默认属性
let aWithDefaults = { x: 1, y: 2, ...a };
// 等价于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
// 打包-还原
let assembled = { x: 1, y: 2, a: 3, b: 4 };
let { x, y, ...z } = assembled;
let reassembled = { x, y, ...z };
P.S.关于打包-还原的实际应用,见react-redux源码解读 | 默认参数与对象解构
另外,还有2个细节:
展开属性只触发(待展开对象的)getter,不触发(目标对象的)setter
尝试展开
null, undefined
不会引发报错,而是忽略掉
例如:
// 拷贝x时会触发getter
let runtimeError = { ...{a: 1}, ...{ get x() { throw new Error('报错') } } };
// 重写x时候不触发setter
let z = { set x(v) { throw new Error('不报错'); }, ...{ x: 1 } }; // No error
四.正则表达式增强
说来话长,1999年ES3引入正则表达式支持,2016年的ES2015增强过一波:
Unicode mode (the u flag):实际应用见JavaScript emoji utils | 正则表达式中的Unicode
sticky mode (the y flag):严格从
lastIndex
指定的位置开始匹配the RegExp.prototype.flags getter:获取正则表达式对象所开启的模式标识(
gimuy
按字母序排列,分别表示全局匹配、忽略大小写、多行匹配、Unicode支持与严格模式)
2017年的ES2018进一步增强:
s (dotAll) flag for regular expressions:点号通配模式,在此模式下,点号可以匹配任意字符(默认点号只能匹配除换行符外的任意字符)
RegExp Lookbehind Assertions:肯定逆序环视,支持向后看
RegExp named capture groups:命名捕获分组
RegExp Unicode Property Escapes:Unicode(序列)属性转义
s (dotAll) flag for regular expressions
不开s
模式的话,.
(点号)能够匹配除换行外的任意字符,换行符有4个:
U+000A LINE FEED (LF)
(\n)
U+000D CARRIAGE RETURN (CR)
(\r)
U+2028 LINE SEPARATOR:行分隔符
U+2029 PARAGRAPH SEPARATOR:段分隔符(与行分隔符一样,都是不可见字符)
例如:
/a.c/.test('abc') === true
/a.c/.test('a\nc') === false
/a.c/.test('a\rc') === false
/a.c/.test('a\u2028c') === false
/a.c/.test('a\u2029c}') === false
要想匹配任意字符的话,只能通过一些技巧绕过,如:
// [^]匹配一个字符,什么都不排除
/a[^]c/s.test('a\nc') === true
// [\s\S]匹配一个字符,任意空白字符和非空白字符
/a[^]c/s.test('a\nc') === true
有了点号通配模式以后,这些换行符都能被点号匹配(像其它语言的正则引擎一样):
const regex = /a.c/s;
regex.test('a\nc') === true
另外,还有两个属性用来获取该模式是否已开启:
regex.dotAll === true
regex.flags === 's'
注意,点号通配模式(s
)并不影响多行匹配模式(m
),二者是完全独立的:
s
:只影响.
(点号)的匹配行为m
:只影响^$
的匹配行为
可以一起用,也互不干扰:
// 不开m时,$匹配串尾
/^c$/.test('a\nc') === false
// 开m之后,$能够匹配行尾
/^c$/m.test('a\nc') === true
// 同时开sm,各司其职
/^b./sm.test('a\nb\nc') === true
P.S.m
模式术语叫增强的行锚点模式(具体见正则表达式学习笔记
| 九.附表【元字符表】【模式控制符表】【特殊元字符表】):
增强的行锚点模式,把段落分割成逻辑行,使得^和$可以匹配每一行的相应位置,而不是整个串的开始和结束位置
RegExp Lookbehind Assertions
正则环视(lookaround)相关的一个特性,环视的特点是不匹配任何字符,只匹配文本中的特定位置:
Lookarounds are zero-width assertions that match a string without consuming anything.
ES2018引入了逆序环视:
(?<=...)
:肯定逆序环视(Positive lookbehind assertions),子表达式能够匹配左侧文本时才成功匹配(?<!...)
:否定逆序环视(Negative lookbehind assertions),子表达式不能匹配左侧文本时才成功匹配
一种向后看的能力,典型应用场景如下:
// 从'$10.53'提取10.53,即捕获左侧是$符的数值
'$10.53'.match(/(?<=\$)\d+(\.\d*)?/)[0] === '10.53'
// 从'$-10.53 $-10 $0.53'提取正值0.53,即捕获左侧不是负号的数值
'$-10.53 $-10 $0.53'.match(/(?<=\$)(?<!-)\d+(\.\d*)?/g)[0] === '0.53'
向前看的能力一直都有,例如:
// (?=…) 肯定顺序环视, 子表达式能够匹配右侧文本
'baaabac'.match(/(?=(a+))a*b\1/)[0] === 'aba'
// (?!…) 否定顺序环视,子表达式不能匹配右侧文本
'testRegexp test-feature tesla'.match(/(?<=\s)(?!test-?)\w+/g)[0] === 'tesla'
具体见ES5规范15.10.2.8 Atom中的NOTE 2与NOTE 3
逆序环视与反向引用
实现上,含逆序环视的正则表达式的匹配顺序是从右向左的,例如:
// 逆序环视,从右向左扫描输入串,所以$2贪婪匹配到了053
'1053'.replace(/(?<=(\d+))(\d+)$/, '[$1,$2]') === '1[1,053]'
// 一般情况,从左向右扫描输入串,贪婪匹配$1为105
'1053'.replace(/^(\d+)(\d+)/, '[$1,$2]') === '[105,3]'
从上例能够发现另一个细节:虽然扫描顺序相反,但捕获分组排序都是从左向右的
此外,逆序环视场景下反向扫描对反向引用有影响,毕竟只能引用已匹配过的内容:
Within a backreference, it is only possible to refer to captured groups that have already been evaluated.
所以要想匹配叠词的话,应该这样做:
/(?<=\1(.))/.test('哈哈') === true
而不是:
/(?<=(.)\1)/.test('哈8') === true
实际上,这里的\1
什么都匹配不到,永远是空串(因为从右向左扫,还没捕获哪来的引用),删掉它也没关系(/(?<=(.))/
)
P.S.关于反向引用与逆序环视的更多信息,见Greediness proceeds from right to left
RegExp named capture groups
常见的日期格式转换场景:
'2017-01-25'.replace(/(\d{4})-(\d{2})-(\d{2})/, '$1/$2/$3') === '2017/01/25'
我们通过$n
来引用对应的捕获到的内容,存在两个问题:
可读性:
$n
仅表示第几个捕获分组,不含其它语义灵活性:一旦正则表达式中括号顺序发生变化,replacement(
$1/$2/$3
)要跟着变
命名捕获分组能够很好的解决这两个问题:
const reDate = /(?<yyyy>\d{4})-(?<mm>\d{2})-(?<dd>\d{2})/;
'2017-01-25'.replace(reDate, '$<yyyy>/$<mm>/$<dd>') === '2017/01/25'
正则表达式中的捕获分组与replacement中的引用都有了额外语义
另外,匹配结果对象身上也有一份命名捕获内容:
let result = reDate.exec('2017-01-25');
const { yyyy, mm, dd } = result.groups;
// 或者
// const { groups: {yyyy, mm, dd} } = result;
`${yyyy}/${mm}/${dd}` === '2017/01/25'
从语法上看,引入了3个新东西:
(?<name>...)
:命名捕获型括号\k<name>
:命名反向引用$<name>
:命名replacement引用,函数形式的replacement把groups
作为最后一个参数,具体见Replacement targets
例如:
P.S.特性不错,语法有点太长了啊,对比(...)
与(?<name>...)
。。。虽说是出于向后兼容考虑
RegExp Unicode Property Escapes
Unicode字符有一些属性,比如π
是希腊文字,在Unicode中对应的属性是Script=Greek
为了支持根据Unicode属性特征匹配字符的场景,提供了两种语法:
\p{UnicodePropertyName=UnicodePropertyValue}
:匹配一个Unicode属性名等于指定属性值的字符\p{LoneUnicodePropertyNameOrValue}
:匹配一个该Unicode属性值为true的字符
P.S.对应的\P
表示补集
注意,都要开u
模式,不开不认
前者适用于非布尔值(non-binary)属性,后者用于布尔值(binary)属性,例如:
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') === true
// Unicode数字
/\p{Number}{2}/u.test('罗马数字和带圈数字Ⅵ㉜') === true
// Unicode版\d
/^\p{Decimal_Number}+$/u.test('') === true
P.S.支持的属性名及值都按Unicode标准来,定义在PropertyAliases.txt、ropertyValueAliases.txt,布尔值属性定义在UTS18 RL1.2
const reEmoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
reEmoji.test('\u{1F469}') === true
P.S.关于binary为什么表示布尔值属性,见1.2 Properties
五.其它小特性
Promise.prototype.finally
Pending
的Promise要么Resolved
要么Rejected
,而有些时候需要的是Resolved || Rejected
,比如只想等到异步操作结束,不论成功失败,此时Promise.prototype.finally
就是最合适的解决方案:
fetch('http://www.example.com').finally(() => {
// 请求回来了(不论成功失败),隐藏loading
document.querySelector('#loading').classList.add('hide');
});
可以在finally
块里做一些清理工作(类似于try-catch-finally
的finally
),比如隐藏loading、关闭文件描述符、log记录操作已完成
之前类似的场景一般通过then(f, f)
来解决,但finally
的特点在于:
没有参数(专职清理,不关心参数)
不论
Resolved
还是Rejected
都触发不影响Promise链的状态及结果(而
then(() => {}, () => {})
会得到Resolved undefined
),除非finally
块里的throw
或者return rejectedPromise
会让Promise链变为Rejected error
例如:
Promise.resolve(1)
.finally(() => 2)
.finally((x) => new Promise((resolve) => {
setTimeout(() => {
resolve(x+1)
}, 3000);
}))
.then(
// 3秒后,log 1
res => console.log(res)
)
Resolved 1
始终没被改变,因为从设计上不希望finally
影响返回值:
Syntactic finally can only modify the return value with an “abrupt completion”: either throwing an exception, or returning a value early. Promise#finally will not be able to modify the return value, except by creating an abrupt completion by throwing an exception (ie, rejecting the promise)
其中,returning a value early
指的是返回Rejected Promise,例如:
Promise.resolve(1)
// returning a value early
.finally(() => Promise.reject(2))
.catch(ex => console.log(ex))
.finally(() => {
// throwing an exception
throw 3;
})
.catch(ex => console.log(ex))
Lifting template literal restriction
模板字符串默认识别(尝试去匹配解释)其中的转义字符:
\u
:Unicode字符序列,如\u00FF
或\u{42}
\x
:十六进制数值,如\xFF
\0
:八进制,如\101
,具体见Octal escape sequences
P.S.实际上,八进制转义序列在模板字面量和严格模式下的字符串字面量都是不合法的:
Octal escapes are forbidden in template literals and strict mode string literals.
对于不合法的转义序列,会报错:
// Uncaught SyntaxError: Invalid Unicode escape sequence
`\uZZZ`
// Uncaught SyntaxError: Invalid hexadecimal escape sequence
`\xxyz`
// Uncaught SyntaxError: Octal escape sequences are not allowed in template strings.
`\0999`
// 更容易出现的巧合
`windowsPath = c:\usrs\xxx\projects`
但是,模板字符串作为ES2015最开放的特性:
标签模板以开放的姿态欢迎库设计者们来创建强有力领域特定语言。这些语言可能看起来不像JS,但是它们仍可以无缝嵌入到JS中并与JS的其它语言特性智能交互。我不知道这一特性将会带领们走向何方,但它蕴藏着无限的可能性,这令我感到异常兴奋!
这种粗暴的默认解析实际上限制了模板字符串的包容能力,例如latex:
let latexDocument = `
\newcommand{\fun}{\textbf{Fun!}} // works just fine
\newcommand{\unicode}{\textbf{Unicode!}} // Illegal token!
\newcommand{\xerxes}{\textbf{King!}} // Illegal token!
Breve over the h goes \u{h}ere // Illegal token!
`
这是一段合法的latex源码,但其中的\unicode
、\xerxes
和\u{h}ere
会引发报错
针对这个问题,ES2018决定对标签模板去掉这层默认解析,把处理非法转义序列的工作抛到上层:
Remove the restriction on escape sequences.
Lifting the restriction raises the question of how to handle cooked template values that contain illegal escape sequences.
例如:
function tag(strs) {
// 解析过的,存在非法转义序列就是undefined
strs[0] === undefined
// 裸的,与输入完全一致
strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`
P.S.关于标签模板的更多信息,请查看模板字符串_ES6笔记3
注意,这个特性仅针对标签模板,普通模板字符串仍然保留之前的行为(遇到非法转义序列会报错):
let bad = `bad escape sequence: \unicode`; // throws early error
六.总结
最实在的特性要数正则表达式相关增强,此外Promise任务模型正在逐步完善、generator与async function擦出了火花、已经广泛应用的展开运算符终于敲定了、模板字符串的包容性限制去掉了一些,使之符合设计初衷
总之,有点着急的JS语言正在往好的方向发展
P.S.ES2019相关信息,见Finished Proposals