原文:TC39, ECMAScript, and the Future of JavaScript
做者:Nicolás Bevacquajavascript
很荣幸可以和 Nicolás Bevacqua 同台分享。Nicolás Bevacqua 分享了《the Future of Writing JavaScript 》,我在其后分享了《面向前端开发者的V8性能优化》。若是想了解更多 V8 知识能够关注个人专栏:V8 引擎。前端
因为 Nicolás Bevacqua 是英文分享,现场由不少听众都没有太明白,会后我联系了 Nicolás Bevacqua 争得大神赞成后将其文章翻译为中文。java
大神微信玩的很溜,很快就学会了抢红包。git
再次感谢 Nicolás Bevacqua 的精彩分享。es6
译文:github
上周,我在中国深圳的腾讯前端大会上发表了与本文同名的演讲。在这篇文章中,我根据 PonyFoo 网站的格式从新编辑了一遍。我但愿你喜欢它!正则表达式
TC39 指的是技术委员会(Technical Committee)第 39 号。它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构。算法
ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展。其中规定了:express
字符串 'A'
为何是 NaN
json
字符串 'A'
为何不等于 NaN
NaN
为何是 NaN
,但却不等于 NaN
并介绍了为何 Number.isNaN
是一个很好的 idea ...
isNaN(NaN) // true isNaN('A') // true 'A' == NaN // false 'A' === NaN // false NaN === NaN // false // … 解决方案! Number.isNaN('A') // false Number.isNaN(NaN) // true
它还解释了正零与负零什么状况下相等,什么状况下不相等。。。
+0 == -0 // true +0 === -0 // true 1/+0 === 1 / -0 // false
并且 js 中还有不少奇技淫巧,例如只使用感叹号、小括号、方括号和加号来编码任何有效的 JavaScript 表达式。能够在 JSFuck 网站了解更多关于如何只使用 +!()[]
编写 JavaScript 代码的技巧。
不论如何,TC39 所作的不懈努力是难能难得的。
TC39 遵循的原则是:分阶段加入不一样的语言特性。一旦提案成熟,TC39 会根据提案中的变更来更新规范。直到最近,TC39 依然依赖基于 Microsoft Word 的比较传统的工做流程。但 ES3 出来以后,他们花了十年时间,几乎没有任何改变,使其达到规范。以后,ES6 又花了四年才能实现。
显然,他们的流程必须改善。
自 ES6 出来以后,他们精简了提案的修订过程,以知足现代化开发的需求。新流程使用 HTML 的超集来格式化提案。他们使用 GitHub pull requests,这有助于增长社区的参与,而且提出的提案数量也增长了。这个规范如今是一个 living standard,这意味着提案会更快,并且咱们也不用等待新版本的规范出来。
新流程涉及四个不一样的 Stage。一个提案越成熟,越有可能最终将其归入规范。
任何还没有提交做为正式提案的讨论、想法变动或者补充都被认为是第 0 阶段的“稻草人”提案。只有 TC39 的成员能够建立这些提案,并且今天就有若干活跃的“稻草人”提案。
目前在 Stage 0 的提案包括异步操做的 cancellation tokens , Zones 做为 Angular 团队的一员,提供了不少建议。Stage 0 包括了不少一直没有进入 Stage 1 的提案。
在这篇文章的后面,咱们将仔细分析一部分提案。
在 Stage 1,提案已经被正式化,并指望解决此问题,还须要观察与其余提案的相互影响。在这个阶段的提案肯定了一个分散的问题,并为这个问题提供了具体的解决方案。
Stage 1 提议一般包括高阶 API 描述(high level AP),使用示例以及内部语义和算法的讨论。这些建议在经过这一过程时可能会发生重大变化。
Stage 1 目前提案的例子包括:Observable、do 表达式、生成器箭头函数、Promise.try。
Stage 2 的提案应提供规范初稿。
此时,语言的实现者开始观察 runtime 的具体实现是否合理。该实现可使用 polyfill 的方式,以便使代码可在 runtime 中的行为负责规范的定义; javascript 引擎的实现为提案提供了原生支持; 或者能够 Babel 这样的编译时编译器来支持。
目前 Stage 2 阶段的提案有 public class fields、private class fields、decorators、Promise#finally、等等。
Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3 的提案不会有太大的改变,在对外发布以前只是修正一些问题。
语言的实现者也应该对此提案感兴趣 - 若是只是提案却没有具体实现去支持这个提案,那么这个提案早就胎死腹中了。事实上,提案至少具备一个浏览器实现、友好的 polyfill或者由像 Babel 这样的构建时编译器支持。
Stage 3 由不少使人兴奋的功能,如对象的解析与剩余,异步迭代器,import() 方法和更好的 Unicode 正则表达式支持。
最后,当规范的实现至少经过两个验收测试时,提案进入 Stage 4。
进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中。
异步函数,Array#includes 和 幂运算符 是 Stage 4 的一些特性。
我(原文做者)建立了一个网站,用来展现当前提案的列表。它描述了他们在什么阶段,并连接到每一个提案,以便您能够更多地了解它们。
网址为 proptt39.now.sh。
目前,每一年都有新的正式规范版本,但精简的流程也意味着正式版本变得愈来愈不相关。如今重点放在提案阶段,咱们能够预测,在 ES6 以后,对该标准的具体修订的引用将变得不常见。
咱们来看一些目前正在开发的最有趣的提案。
在介绍 Array#includes
以前,咱们不得不依赖 Array#indexOf
函数,并检查索引是否超出范围,以肯定元素是否属于数组。
随着 Array#includes
进入 Stage 4,咱们可使用 Array#includes
来代替。它补充了 ES6 的 Array#find
和 Array#findIndex
。
[1, 2].indexOf(2) !== -1 // true [1, 2].indexOf(3) !== -1 // false [1, 2].includes(2) // true [1, 2].includes(3) // false
当咱们使用 Promise 时,咱们常常考虑执行线程。咱们有一个异步任务 fetch
,其余任务依赖于 fetch
的响应,但在收到该数据以前程序时阻塞的。
在下面的例子中,咱们从 API 中获取产品列表,该列表返回一个 Promise
。当 fetch 相应以后,Promise 被 resolve。而后,咱们将响应流做为 JSON 读取,并使用响应中的数据更新视图。若是在此过程当中发生任何错误,咱们能够将其记录到控制台,以了解发生了什么。
fetch('/api/products') .then(response => response.json()) .then(data => { updateView(data) }) .catch(err => { console.log('Update failed', err) })
异步函数提供了语法糖,能够用来改进咱们基于 Promise
的代码。咱们开始逐行改变以上基于 Promise 的代码。咱们可使用 await
关键字。当咱们 await
一个 Promise 时,咱们获得 Promise 的 fulled 状态的值。
Promise 代码的意思是:“我想执行这个操做,而后(then)在其余操做中使用它的结果”。
同时,await
有效地反转了这个意思,使得它更像:“我想要取得这个操做的结果”。我喜欢,由于它听起来更简单。
在咱们的示例中,响应对象是咱们以后获取的,因此咱们将等待(await
)获取(fetch
)操做的结果,并赋值给 response
变量,而不是使用 promise
的 then
。
原文:we’ll flip things over and assigned the result of await
fetch
to the response
variable
+ const response = await fetch('/api/products') - fetch('/api/products') .then(response => response.json()) .then(data => { updateView(data) }) .catch(err => { console.log('Update failed', err) })
咱们给 response.json()
一样的待遇。咱们 await
上一次的操做并将其赋值给 data
变量。
const response = await fetch('/api/products') + const data = await response.json() - .then(response => response.json()) .then(data => { updateView(data) }) .catch(err => { console.log('Update failed', err) })
既然 then
链已经消失了,咱们就能够直接调用 updateView
语句了,由于咱们已经到了以前代码中的 Promise then 链的尽头,咱们不须要等待任何其余的 Promise。
const response = await fetch('/api/products') const data = await response.json() + updateView(data) - .then(data => { - updateView(data) - }) .catch(err => { console.log('Update failed', err) })
如今咱们可使用 try/catch
块,而不是 .catch
,这使得咱们的代码更加语义化。
+ try { const response = await fetch('/api/products') const data = await response.json() updateView(data) + } catch(err) { - .catch(err => { console.log('Update failed', err) + } - )}
一个限制是 await
只能在异步函数内使用。
+ async function run() { try { const response = await fetch('/api/products') const data = await response.json() updateView(data) } catch(err) { console.log('Update failed', err) } + }
可是,咱们能够将异步函数转换为自调用函数表达式。若是咱们将顶级代码包在这样的表达式中,咱们能够在代码中的任何地方使用 await
表达式。
一些社区但愿原生支持顶级块做用于的 await
,而另一些人则认为这会对用户形成负面影响,由于一些库可能会阻塞异步加载,从而大大减缓了咱们应用程序的加载时间。
+ (async () => { - async function run() { try { const response = await fetch('/api/products') const data = await response.json() updateView(data) } catch(err) { console.log('Update failed', err) } + })() - }
就我的而言,我认为在 JavaScript 性能中已经有足够的空间来应对这种愚蠢的事情,来优化初始化的库使用 await
的行为。
请注意,您也能够在 non-promise 的值前面使用 await
,甚至编写代码 await (2 + 3)
。在这种状况下,(2 + 3)
表达的结果会被包在 Promise 中,做为 Promise 的最终值。5
成为这个 await
表达式的结果。
请注意,await
加上任何 JavaScript 表达式也是一个表达式。这意味着咱们不限制 await
语句的赋值操做,并且咱们也能够把 await
函数调用做为模板文字插值的一部分。
`Price: ${ await getPrice() }`
或做为另外一个函数调用的一部分...
renderView(await getPrice())
甚至做为数学表达式的一部分。
2 * (await getPrice())
最后,无论它们的内容如何,异步函数老是返回一个 Promise。这意味着咱们能够添加 .then
或 .catch
等异步功能,也可使用 await
获取最终的结果。
const sleep = delay => new Promise(resolve => setTimeout(resolve, delay) ) const slowLog = async (...terms) => { await sleep(2000) console.log(...terms) } slowLog('Well that was underwhelming') .then(() => console.log('Nailed it!')) .catch(reason => console.error('Failed', reason))
正如您所指望的那样,返回的 Promise 与 async
函数返回的值进行运算,或者被 catch 函数来处理任何未捕获的异常。
异步迭代器已经进入了 Stage 3。在了解异步迭代器以前,让咱们简单介绍一下 ES6 中引入的迭代。迭代能够是任何遵循迭代器协议的对象。
为了使对象能够迭代,咱们定义一个 Symbol.iterator
方法。迭代器方法应该返回一个具备 next
方法的对象。这个对象描述了咱们的 iterable
的顺序。当对象被迭代时,每当咱们须要读取序列中的下一个元素时,将调用 next
方法。value
用来获取序列中每个对象的值。当返回的对象被标记为 done
,序列结束。
const list = { [Symbol.iterator]() { let i = 0 return { next: () => ({ value: i++, done: i > 5 }) } } } [...list] // <- [0, 1, 2, 3, 4] Array.from(list) // <- [0, 1, 2, 3, 4] for (const i of list) { // <- 0, 1, 2, 3, 4 }
可使用 Array.from
或使用扩展操做符使用 Iterables
。它们也能够经过使用 for..of
循环来遍历元素序列。
异步迭代器只有一点点不一样。在这个提议下,一个对象经过 Symbol.asyncIterator
来表示它们是异步迭代的。异步迭代器的方法签名与常规迭代器的约定略有不一样:该 next
方法须要返回 包装了 { value, done }
的 Promise
,而不是 { value, done }
直接返回。
const list = { [Symbol.asyncIterator]() { let i = 0 return { next: () => Promise.resolve({ value: i++, done: i > 5 }) } } }
这种简单的变化很是优雅,由于 Promise 能够很容易地表明序列的最终元素。
异步迭代不能与数组扩展运算符、Array.from
、for..of
一块儿使用,由于这三个都专门用于同步迭代。
这个提案也引入了一个新的 for await..of
结构。它能够用于在异步迭代序列上语义地迭代。
for await (const i of items) { // <- 0, 1, 2, 3, 4 }
请注意,该 for await..of
结构只能在异步函数中使用。不然咱们会获得语法错误。就像任何其余异步函数同样,咱们也能够在咱们的循环周围或内部使用 try/catch
块 for await..of
。
async function readItems() { for await (const i of items) { // <- 0, 1, 2, 3, 4 } }
更进一步。还有异步生成器函数。与普通生成器函数有些类似,异步生成器函数不只支持 async
await
语义,还容许 await
语句以及 for await..of
。
(原文第一段:The rabbit hole goes deeper of course. 这是爱丽丝梦游仙境的梗吗?)
async function* getProducts(categoryUrl) { const listReq = await fetch(categoryUrl) const list = await listReq.json() for (const product of list) { const productReq = await product.url const product = await productReq.json() yield product } }
在异步生成器函数中,咱们可使用 yield*
与其余异步发生器和普通的发生器一块儿使用。当调用时,异步生成器函数返回异步生成器对象,其方法返回包裹了 { value, done }
的 Promise,而不是 { value, done }
。
最后,异步生成器对象能够被使用在 for await..of
,就像异步迭代同样。这是由于异步生成器对象是异步迭代,就像普通生成器对象是普通的迭代。
async function readProducts() { const g = getProducts(category) for await (const product of g) { // use product details } }
从 ES6 开始,咱们使用 Object.assign
将属性从一个或多个源对象复制到一个目标对象上。在下一个例子中,咱们将一些属性复制到一个空的对象上。
Object.assign( {}, { a: 'a' }, { b: 'b' }, { a: 'c' } )
对象解构(spread)提议容许咱们使用纯语法编写等效的代码。咱们从一个空对象开始,Object.assign
隐含在语法中。
{ ...{ a: 'a' }, ...{ b: 'b' }, ...{ a: 'c' } } // <- { a: 'c', b: 'b' }
和对象解构相反的还有对象剩余,相似数组的剩余参数。当对对象进行解构时,咱们可使用对象扩展运算符将模式中未明确命名的属性重建为另外一个对象。
在如下示例中,id 显式命名,不会包含在剩余对象中。对象剩余(rest)能够从字面上读取为“全部其余属性都转到一个名为 rest 的对象”,固然,变量名称供您选择。
const item = { id: '4fe09c27', name: 'Banana', amount: 3 } const { id, ...rest } = item // <- { name: 'Banana', amount: 3 }
在函数参数列表中解析对象时,咱们也可使用对象剩余属性。
function print({ id, ...rest }) { console.log(rest) } print({ id: '4fe09c27', name: 'Banana' }) // <- { name: 'Banana' }
ES6 引入了原生 JavaScript 模块。与 CommonJS 相似,JavaScript 模块选择了静态语法。这样开发工具备更简单的方式从静态源码中分析和构建依赖树,这使它成为一个很好的默认选项。
import markdown from './markdown' // … export default compile
然而,做为开发人员,咱们并不老是知道咱们须要提早导入的模块。对于这些状况,例如,当咱们依赖本地化来加载具备用户语言的字符串的模块时,Stage 3 的动态 import()
提案就颇有用了。
import()
运行时动态加载模块。它为模块的命名空间对象返回 Promise,当获取该对象时,系统将解析和执行所请求的模块及其全部依赖项。若是模块加载失败,Promise 将被拒绝。
import(`./i18n.${ navigator.language }.js`) .then(module => console.log(module.messages)) .catch(reason => console.error(reason))
未完。。。。