柚木

[译] TC39,ECMAScript 和 JavaScript 的未来(Part 1)

原文:TC39, ECMAScript, and the Future of JavaScript
作者:Nicolás Bevacqua

译者序

很荣幸能够和 Nicolás Bevacqua 同台分享。Nicolás Bevacqua 分享了《the Future of Writing JavaScript 》,我在其后分享了《面向前端开发者的V8性能优化》。如果想了解更多 V8 知识可以关注我的专栏:V8 引擎

由于 Nicolás Bevacqua 是英文分享,现场由很多听众都没有太明白,会后我联系了 Nicolás Bevacqua 争得大神同意后将其文章翻译为中文。

大神微信玩的很溜,很快就学会了抢红包。

再次感谢 Nicolás Bevacqua 的精彩分享。

译文:

上周,我在中国深圳的腾讯前端大会上发表了与本文同名的演讲。在这篇文章中,我根据 PonyFoo 网站的格式重新编辑了一遍。我希望你喜欢它!

TC39 是什么?

TC39 指的是技术委员会(Technical Committee)第 39 号。它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构。

ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展。其中规定了:

  • 字符串 'A' 为什么是 NaN
  • 字符串 '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。一个提案越成熟,越有可能最终将其纳入规范。

Stage 0

任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的“稻草人”提案。只有 TC39 的成员可以创建这些提案,而且今天就有若干活跃的“稻草人”提案。

目前在 Stage 0 的提案包括异步操作的 cancellation tokensZones 作为 Angular 团队的一员,提供了很多建议。Stage 0 包括了很多一直没有进入 Stage 1 的提案。

在这篇文章的后面,我们将仔细分析一部分提案。

Stage 1

在 Stage 1,提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响。在这个阶段的提案确定了一个分散的问题,并为这个问题提供了具体的解决方案。

Stage 1 提议通常包括高阶 API 描述(high level AP),使用示例以及内部语义和算法的讨论。这些建议在通过这一过程时可能会发生重大变化。

Stage 1 目前提案的例子包括:Observabledo 表达式、生成器箭头函数、Promise.try

Stage 2

Stage 2 的提案应提供规范初稿。

此时,语言的实现者开始观察 runtime 的具体实现是否合理。该实现可以使用 polyfill 的方式,以便使代码可在 runtime 中的行为负责规范的定义; javascript 引擎的实现为提案提供了原生支持; 或者可以 Babel 这样的编译时编译器来支持。

目前 Stage 2 阶段的提案有 public class fieldsprivate class fieldsdecoratorsPromise#finally、等等。

Stage 3

Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3 的提案不会有太大的改变,在对外发布之前只是修正一些问题。

语言的实现者也应该对此提案感兴趣 - 如果只是提案却没有具体实现去支持这个提案,那么这个提案早就胎死腹中了。事实上,提案至少具有一个浏览器实现、友好的 polyfill或者由像 Babel 这样的构建时编译器支持。

Stage 3 由很多令人兴奋的功能,如对象的解析与剩余异步迭代器import() 方法和更好的 Unicode 正则表达式支持

Stage 4

最后,当规范的实现至少通过两个验收测试时,提案进入 Stage 4。

进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中。

异步函数Array#includes幂运算符 是 Stage 4 的一些特性。

保持最新 Staying Up To Date

我(原文作者)创建了一个网站,用来展示当前提案的列表。它描述了他们在什么阶段,并链接到每个提案,以便您可以更多地了解它们。

网址为 proptt39.now.sh

目前,每年都有新的正式规范版本,但精简的流程也意味着正式版本变得越来越不相关。现在重点放在提案阶段,我们可以预测,在 ES6 之后,对该标准的具体修订的引用将变得不常见。

提案 Proposals

我们来看一些目前正在开发的最有趣的提案。

Array#includes (Stage 4)

在介绍 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

异步函数(Stage 4)

当我们使用 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)

异步迭代器已经进入了 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
  }
}

对象解构与剩余(Stage 3)

从 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' }

动态 import()(Stage 3)

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))

未完。。。。