ES2018
一.概览
2个主特性:
Asynchronous Iteration
Rest/Spread Properties
正则表达式相关的4个小特性:
RegExp Lookbehind Assertions
RegExp Unicode Property Escapes
RegExp named capture groups
s (dotAll) flag for regular expressions
其它:
Promise.prototype.finally
Lifting template literal restriction
二.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 3002second 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 tolet { 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时会触发getterlet runtimeError = { ...{a: 1}, ...{ get x() { throw new Error('报错') } } };// 重写x时候不触发setterlet 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 === trueregex.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 leftRegExp 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 EscapesUnicode字符有一些属性,比如π是希腊文字,在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喜报,Emoji问题也终于有终极解决方案了:
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 PropertiesP.S.Unicode字符表见**五.其它小特性**Promise.prototype.finallyPending的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 sequencesP.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参考资料ECMAScript regular expressions are getting better!Template Literal RevisionECMAScript regular expressions are getting better!
更多相关文章
- Java 中字符集的编解码
- 008. 字符串转换整数 (atoi) | Leetcode题解
- 003. 无重复字符的最长子串 | Leetcode题解
- jQuery——将title属性用作悬停的文本,但只在同一父类中使用。
- Jquery对选取到的元素显示指定的长度,对于的字符串用“...”显示
- jQuery编程基础精华02(属性、表单过滤器,元素的each,表单选择器,子元
- 将字符串数组发布到.net-core mvc
- js或Jquery中判断字符串中是否有换行符或回车符/n
- 通过],[和创建json对象来分割字符串