59

JS 规范又双叒要更新了

 4 years ago
source link: https://www.tuicool.com/articles/Rba2MnI
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

JavaScript 目前依旧很受欢迎。如 GitHub 的报告 Octoverse 中的 图表 所示,JavaScript 是 GitHub 多年来最常用的语言。此外,Stack Overflow 最近的 “2019 开发者调查报告” 也将 JavaScript 列为最受欢迎的技术。

QzeIbey.jpg!web

GitHub 最流行语言的变迁

ECMAScript 2015 和随后的 ECMAScript 规范看起来也被众多开发者顺利接受了。

R7rAveJ.jpg!web

本文将介绍 ECMAScript 2018 的主要功能,以及预计将包含在 ECMAScript 2019 和 2020 规范中的提案。

注明:本文的内容基于截止 2019 年 6 月的已知信息。文章中的内容和事实可能会随时间推移发生变化。

一些改动和新闻

有些改变不会对 JavaScript 的语言层面产生直接影响,但可能会带来间接影响,例如影响环境和生态系统等。

TC39 的变化

TC39 是一个讨论 JavaScript 标准规范的技术委员会,已决定从 2019 年开始改变其运营结构。TC39 每年召开六次会议,规模已经发展到每次有 40 到 60 人参会的程度。

这个委员会以前是主席和副主席的运营结构,现在改为由三位联合主席(Aki Braun(PayPal)、Brian Terlson(微软)和 Yulia Startsev(Mozilla))共同负责的架构。他们还在 2019 年 3 月开设了 官方网站

在 TC39 联合主席 Aki Braun 撰写的文章“TC39 的一年(多)历程”中介绍了 TC39 会议的进展和成果。

2018 年 7 月初,npm 加入了 ECMA InternationalTC39

SharedArrayBuffer

Meltdown 和 Spectre 安全漏洞 影响,浏览器厂商已更改其默认设置,禁用了自 2018 年 1 月 5 日起使用的 SharedArrayBuffer 对象

除 Chrome 之外,其他浏览器依旧保持着这种设置。Chrome 67 版之后通过 站点隔离 重新激活了该对象(参阅 https://bugs.chromium.org/p/chromium/issues/detail?id=821270 )。

微软 Edge 改用 chromium 引擎

2018 年 12 月 6 日,微软宣布将其 Edge 浏览器转向 Chromium 引擎(参阅 “微软 Edge,通过开源协作让 Web 受益” ,震惊业界。

在 2019 年 1 月 29 日举行的 TC39 会议期间,场下 微软问答环节 针对 Edge 的引擎转换披露了以下事实:

  • 没有开源旧引擎的计划。

  • 现有 JavaScript 引擎 ChakraCore 将继续更新,但没有长期更新计划。

ChakraCore 团队成员 Limin Zhu 评论说:除了浏览器,还有很多项目在使用 ChakraCore。因此尽管 Edge 改变了方向,我们的团队仍将继续支持 ChakraCore(参阅 “微软 Edge 向 Chromium 迁移以及 ChakraCore 的未来” )。

可以从微软 Edge 内部 通道站点 下载基于 chromium 的 Edge(Canary/Dev/Beta 版本)。

微软将 Edge 转向 Chromium 引擎的目的和未来计划可参阅这篇文章 “微软 Edge 与开源 Chromium:我们的意图”

从开发者的角度来看,微软 Edge 的这种转变可以减轻跨浏览器开发的负担和困难。

但是从 Web 生态系统的角度来看,此举可能会带来令人担忧的结果。因为这会减少浏览器的多样性。

你可能还记得 Internet Explorer 处于垄断地位的时代,当时许多网站只支持 Internet Explorer;类似地,基于 chromium 的浏览器越来越多也不是什么好事情。

有关多样性的担忧,请参阅文章 “Chrome 正在变成新的 Internet Explorer 6“ 和 Mozilla 的 “再见,EdgeHTML”

模块支持

ECMAScript 2015 引入了模块支持,现在模块作为基本功能已经广泛应用。

接下来看看动态 import() 语法和原生模块支持的概况与当前状态,进一步扩展模块的应用范围。

动态 import()

基于 Promise 的 import() 语法 可以动态加载模块。之前该提案在第 3 阶段停留了一阵儿,但最终于 6 月 6 日达到 “第 4 阶段” 并进入 ECMAScript 2020 规范。

复制代码

import("./myModule.mjs")
.then(module=>{
...
});
// using async/await
(async() => {
constmodule=awaitimport("./myModule.mjs");
...
})();

Firefox 从 60 版开始可以设置 javascript.options.dynamicImport 标志(flag)来使用 import() 语法,Firefox 67 开始默认启用该设置。

微软 Edge(旧引擎版本)尚不支持 import() 语法,但是基于 chrome 的 Edge 发布时可能会提供支持。

原生模块加载

从 2018 年 5 月发布的 Firefox 60 开始,Firefox 可以无需标志就使用原生模块(ESM)(参阅文章 “Firefox 60——模块和其他更新” )。2017 年 9 月发布的 Node.js 8.5.0 实验性地支持 ESM。

Node.js 中的 ESM 需要–experimental-modules 标志,如下例所示。在这)情况下,CommonJS 的’request()'将被禁用以加载模块。

复制代码

node--experimental-modulesmy-app.mjs

Node.js 基金会为提供 ESM 的官方支持组建了“模块团队”。模块团队的工作分为 4 个阶段:

  • 阶段 0:从当前 Node 分支,但删除了 Node 8.5.0 之后版本中的大部分–experimental-modules 实现。

  • 阶段 1:添加“最小内核”,凡是工作组认为会在新的 ES 模块实现中出现的模块都会包含进去。

  • 阶段 2:在实现中加入足够多的功能,这些功能应该对主流用户有益,从而构建出最小可行产品。

  • 阶段 3:改善用户体验并扩展 MVP。

这项工作目前正处于 第 3 阶段

ECMAScript 2018

ECMAScript 2018 于 2018 年 6 月宣布。

异步迭代器

异步运算符枚举异步流数据,其操作类似于典型的运算符,并使用语法形式 for — await — of。异步运算符和普通运算符之间的区别在于前者返回的是 Promise 对象。

复制代码

asyncfunctiontest(){
// Regular Iterator
for(constioflist) ...
// Async Iterator
forawait(constiofasyncList) ...
}

处理异步调用流时可以创建异步运算符工厂。

复制代码

// example from: https://jakearchibald.com/2017/async-iterators-and-generators/
asyncfunction*asyncRandomNumbers(){
consturl="https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain&rnd=new";
while(true) {
constresponse =awaitfetch(url);
consttext =awaitresponse.text();
yieldNumber(text);
}
}
(asyncfunction(){
leti =0;
forawait(constnumberofasyncRandomNumbers()) {
console.log(++i,"=>", number);
if(i ===10)break;
}
})();
// 1 "=>" 65
// 2 "=>" 62
// ...

对象 Rest/Spread 属性

与 ECMAScript 2015 中的 Rest 参数和 Spread 运算符规范一样,此提案引入了对象字面量(literal)的对象解析分配和传播属性。

复制代码

// Rest property
let {x,y, ...z } = {x:1,y:2, a:3, b:4};
x;// 1
y;// 2
z;// { a: 3, b: 4 }
// Spread property
let n = {x,y, ...z };
n;// { x: 1, y: 2, a: 3, b: 4 }

删除模板字面量语法限制

模板字面量删除了对转义序列使用的限制。

标记模板字面量是接收模板并返回修改后的字符串的函数。传递给函数的字符串可以是以下类型之一:

  • Cooked:转义序列已被解释。

  • Raw:转义序列是普通文本。模板字符串中的非解释值由 String.raw() 方法处理。

复制代码

functiontagFunc(str, substs){
returnstr;
}
constresult = tagFunc`\u{4B}`;
result;// ["K"]
result.raw;// ["\u{4B}"]

以前,如果模板在反斜杠后有一些字符序列会被视为非法,并且不返回原始字符串。

  • \u:Unicode(例如\u004B)

  • \x:十六进制(例如\x4B)

  • \positive:八进制(例如\141)

ECMAScript 2018 移除了与转义序列相关的所有语法限制,并以原始形式返回字符串。在这种情况下,解释的值返回 undefined。

复制代码

constresult= tagFunc`\131`;
result;// [undefined]
result.raw;// ["\131"]

查看 http://2ality.com/2016/09/template-literal-revision.html 了解有关模板字面量的问题和解决方案的更多信息。

Promise.prototype.finally

与 try…catch 语句的 finally 语法一样,此提案引入了对 Promise 对象的类似用法。

finally 语法是在最后无条件执行的代码块,与 Promise 对象的处理状态(‘resolve’或’reject’)无关。调用 Promise 对象后,无论结果如何都将执行此代码块。

复制代码

letisLoading =true;
fetch("https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain&rnd=new")
.then(function(res){
if(res) {
returnres.text();
}
thrownewTypeError("Error");
})
.then(function(text){/* Success */})
.catch(function(error){/* Error */})
.finally(function(){
// will be performed regardless success or error
isLoading =false;
});

查看 MDN 的 Promise.prototype.finally 文档 了解更多信息。

正则表达式

新增了几个正则表达式提案。

  • 正则表达式的 s 标志 。点(.)能匹配所有字符,但\r 和\n 是例外。为了解决这个问题引入了新的 s 标志。

复制代码

//previous
/./test("a");//true
/./.test("\n");//false
//dotAll flag
/./s.test("\n");//true
  • RegEx 命名捕获组 。它可以命名一个捕获组。(? <name> pattern) 将 添加到捕获组模式,然后使用该名称作为捕获的引用。

复制代码

constrx = /(?<year>[0-9]{4})-(?<month>[0-9]{2})/;
constmatch= rx.exec('2018-03');
match.groups.year;// 2018
match.groups.month;// 03
  • RegExp Lookbehind 断言 。在正则表达式中,一个模式的值由字符串跟随(先行)或不跟随(负向先行)。相比之下,这个提案提供了寻找特定模式以前进(后行)或不前进(负向后行)的能力。

复制代码

//positive lookahead
/aa(?=bb)/.test("aabb");//true
//negative lookahead
/aa(?!bb)/.test("aac");//true
//positive lookbehind
/(?<=aa)bb/.test("aabb");//true
//negative lookbehind
/(?<!=aa)bb/.test("bbbb");//true
  • RegEx Unicode 属性转义 。Unicode 属性转义是在设置了 u 标志的正则表达式中新增的可用转义序列类型。

复制代码

/^\p{White_Space}+$/u.test('\t \n\r');//true/^\p{Script=Greek}+$/u.test('μετά');//true

ECMAScript 2019

ECMAScript 2019 尚处于候选草案状态。根据之前版本的发布日期推算,2019 最终版本预计将在 2019 年 6 月左右公布。

Array.prototype.flat()/ Array.prototype.flatMap()

Array.prototype.flat() 方法和 Array.prototype.flatMap() 方法以递归方式查找指定深度的子数组元素,并创建一个连接到其中的新数组。

复制代码

// Array.prototype.flat
[1,2, [3,4]].flat();// [1, 2, 3, 4]
[1,2, [3,4, [5,6]]].flat(1);// [1, 2, 3, 4, [5, 6]]
// Array.prototype.flatMap
[1,2,3,4].map(x => [x *2]);// [[2], [4], [6], [8]]
[1,2,3,4].flatMap(x => [x *2]);// [2, 4, 6, 8]

关于 **.flat()** 命名的小故事:

最初使用的名称是“ .flatten() ”。但是,MoTools(一个应用广泛的遗留库)有一个类似的名称“ array.prototype.flatten() ”,提供了类似的功能。如果继续使用“.flatten()”,可能会在使用 MoTools 的网站上出问题。

为了避免潜在的冲突, .flatten() 被改名为 .flat() 。此外“.smoosh()“也是候选之一;还有人建议改变 ECMAScript 2019 中 MoTools 的 flatten() 行为(参阅 https://github.com/mootools/mootools-core/issues/2797 )。但这可能让许多不再更新的网站崩溃,Tom Dale 的推文指出了这一点(他参与了 Ember 和 SproutCore 的开发)。

rQnEzuY.jpg!web

https://twitter.com/tomdale/status/971288054966247430

Object.fromEntries()

Object.fromEntries() 将键值对列表转换为对象。

复制代码

constentries =newMap([
['foo','bar'],
['baz',42]
]);
Object.fromEntries(entries);// { foo: "bar", baz: 42 }

String.prototype.trimStart()/ .trimEnd()

删除字符串开头的空格(.trimLeft())或删除字符串末尾的空格(.trimRight())。

复制代码

const greeting =" Hello world! ";
greeting.trimStart();//"Hello world! "
greeting.trimEnd();//" Hello world!"

Symbol.prototype.description 属性

Symbol.prototype.description 属性返回 Symbol 对象的可选只读描述。

复制代码

// will return 'Test Description'
Symbol("Test Description").description;

可选的 catch 绑定

可选的 [catch 绑定提案](( https://github.com/tc39/proposal-optional-catch-binding ) 是说,如果 try…catch 语句中的 catch 语法中未使用参数,则允许省略参数。

复制代码

// Traditional way
try{ ··· }catch(e) { ··· }
// Optional catch binding
// if don't need the use of parameter, it can be omitted
try{ ··· }catch{ ··· }

Array.prototype.sort() 稳定性

当对具有 10 个以上元素的数组排序时,Array.prototype.sort() 方法使用了不稳定的快速排序算法。为了确保数组正确对齐,ECMAScript 2019 对 Array.prototype.short() 使用了 Timsort 算法。

此规范目前适用于所有 JavaScript 引擎。但基于 ChakraCore 的微软 Edge 对包含超过 512 个元素的数组会出现排序错误。

下面的截图显示了 Edge 和 Firefox 上的稳定性测试结果。如图所示,Edge 出错了。

yeYz6bF.jpg!web

稳定性测试结果:(左)Edge 17.17134/(右)Firefox 66.0.2

更详细的讨论参阅: https://docs.google.com/presentation/d/1mHvxDciqsAchhjepMZlU5fn1DBvglCXSjDWUEtsPGvI/edit#slide=id.g41da6c5107_0_0

格式良好的 JSON.stringify

RFC 8259 指定要编码为 UTF-8 格式的 JSON 文本用于 JSON 对象数据交换。但是当使用 JSON.stringify() 时,一些 UTF-16 代码(从 0xD800 到 0xDFFFF 的字符被归类为“代理”)不会编码为 UTF-8。

ECMAScript 2019 会返回一个转义序列,而不是返回一个无效的、Unicode 字符串,如下图所示(Edge)。

zqYnAf3.jpg!web

Edge 和 Firefox 上的 JSON.stringify() 结果

更多详细信息请查看 提案文档

包含 JSON

ECMAScript 声明 JSON 是 JSON.parse 的一个子集,但事实并非如此,因为 JSON 字符串可以包含未转义的 U+2028 LINE SEPARATOR 和 U+2029 PARAGRAPH SEPARATOR 字符。

该提案建议扩展 ECMA-262,允许这些字符不将 JSON 分解为 ECMAScript 的子集。

复制代码

//ifECMAissuperset of JSON, these must betrue
eval('"\u2028"') ==="\u2028"//true
eval('"\u2029"') ==="\u2029"//true

详细信息请查看 提案文档

Function.prototype.toString 修订

根据 ECMAScript 2016 中的定义,Function.prototype.toString() 的结果可能因引擎而异。ECMAScript 2019 确保返回定义的原始代码(参阅 https://tc39.es/Function-prototype-toString-revision/ )。

返回函数中定义的代码时,ECMAScript 2019 使用以下算法返回函数中定义的代码字符串:

  • 换行符:\r\n(Windows)或\n(macOS)都以 Unix 样式返回\n。

  • 内置函数:未通过 ECMAScript 定义的所有代码(主要是内置函数)将作为 [native code] 返回。

复制代码

isNaN.toString();// "function isNaN() { [native code] }"

NZrEZnu.jpg!web

不同浏览器中“isNaN.toString()”的结果:

  • 通过 Function 和 GeneratorFunction 动态创建的函数:引擎必须创建适当的源代码并将其附加到函数上。

  • 其他情况下:抛出 TypeError。

更多详细信息查看 http://2ality.com/2016/08/function-prototype-tostring.html

ECMAScript 2020

截至 2019 年 3 月 1 日,TC39 repo master 分支已升级为 ECMAScript 2020(参阅 提交日志 )。

此时,ECMAScript 2020 中已完成的提案(阶段 4)只有 String.prototype.matchAll() 和 import(),但将来完成提案的列表会加入更多项目。

String.prototype.matchAll() 方法与 String.prototype.match() 的工作方式类似。前者与 g(global)/y(sticky) 标志一起使用时,返回包含匹配字符串和匹配详细信息的迭代器。

复制代码

const str ="test1test2";
const rx = /t(e)(st(\d?))/g;
str.match(rx); // ["test1","test2"]
for(constmatchof str.matchAll(rx)) {
//1: ["test1","e","st1","1", index:0,input:"test1test2"]
//2: ["test2","e","st2","2", index:5,input:"test1test2"]
match;
}

一些新的或未完成的提案

下面来看一些尚未进入最后阶段的有趣提案。

globalThis

通常,访问顶级对象要通过浏览器环境中的“window”对象。

作为执行环境的扩展,新提案中访问顶级对象的方式也发生了变化。

  • 在 Node.js 环境中,通过“global”对象访问顶级对象。

  • HTML5 规范有“Window”和“WindowProxy”,而 ECMAScript 2015 规范中这两个对象都能用来访问顶级对象。

查看 https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Inner_and_outer_windows 了解 Window 和 WindowProxy 对象的差异。

以下代码是在所有环境下获取顶级对象(“global”)引用的最简单方法。但是这种方法会导致 Chrome 应用中的内容安全政策(CSP)违规。(参阅 https://github.com/paulmillr/es6-shim/issues/301 )。

复制代码

varglobal=Function('return this')();

知名的 ES6 兼容 shim 库—— ES6 shim ,使用下面的函数来获取全局对象,这是目前常用且最好的方法。

复制代码

// https://github.com/paulmillr/es6-shim/blob/003ee5d15ec1b05ae2ad5ddad3c02fcf8c266e2c/es6-shim.js#L176
vargetGlobal =function(){
/* global self, window, global */
// the only reliable means to get the global object is
// `Function('return this')()`
// However, this causes CSP violations in Chrome apps.
if(typeofself !=='undefined') {returnself; }
if(typeofwindow!=='undefined') {returnwindow; }
if(typeofglobal !=='undefined') {returnglobal; }
thrownewError('unable to locate global object');
};

新提案“globalThis”提供了访问顶级对象的方法,摆脱了与环境的关联性。该提案目前处于第 3 阶段,尚未最终确定。但在 Chrome 71 和 Firefox 65 以及 Node.js 12 中,globalThis 对象可以按如下方式使用:

复制代码

globalThis.setTimeout;//window.setTimeout

关于“globalThis”这个名字:

与“Array.prototype.flat()”案例类似,这个提案一开始命名为“global”。但是使用这个名称会让 flickr.com 崩溃( https://github.com/tc39/proposal-global/issues/20),所以就改为“globalThis”了。

类字段声明

Babel 7.1.0(2018 年 9 月 17 日发布)版本开始可以使用类字段声明。但这个提案尚未达到最后阶段,此时仍处于“第 3 阶段”。

该提案以更直观,更简单的方式为类变量引入了声明性语法。

初始化(Initialization):通过构造函数初始化实例变量。

复制代码

classMyClass{
constructor() {
this.x =1;
this.y =2;
}
}

例变量可以使用类字段定义为以下代码的 //Initializer 部分,初始化区域在构造函数运行之前运行。

复制代码

classMyClass{
// Initializer
x =1;
y =2;
log =console.log("Initializer");
constructor() {
console.log("Constructor:",this.x,this.y);
}
}
newMyClass();
// Initializer
// Constructor: 1 2

私有声明(private declaration):以前 JavaScript 没有提供声明“private”的方式,许多开发者使用下划线(“_”)前缀作为约定。但这并不能真正实现私有化(不过的确有一种方法让变量或方法私有化运行)。

复制代码

functionMyClass(){
this._x =1;
this._y =2;
}
MyClass.prototype.getX =function(){
returnthis._x;
}

私有声明器使用数字符号(#)作为前缀来明确声明它是私有的。以“#”开头的变量或方法只能在类块中访问。

复制代码

classMyClass{
#foo;// private field
constructor(foo) {
this.#foo = foo;
}
incFoo() {
this.#foo++;
}
}

声明和访问:以下是以各种形式的声明和访问类字段的简单示例。

复制代码

classMyClass{
A =1;// (a) instance field
staticB =2;// (b) static class field
#C = 3; // (c) private field
getPrivate() {
returnthis.#C;
}
}
newMyClass().A;// 1
MyClass.B;// 2
newMyClass().getPrivate();// 3

更多详细信息请查看 http://2ality.com/2017/07/class-fields.html

内置模块

内置模块规范现在处于“第 1 阶段”,与 ESM 一样。与通常 ESM 的不同之处在于它们是“内置的”,并随浏览器本身一起分发。

内置模块不直接对全局暴露。它们仅通过导入语法可用。如果浏览器支持内置模块,则使用“std:”前缀 + 模块名称导入这些模块,如下所示。此示例中加载的是 KV Storage 模块

复制代码

import{storage, StorageArea}from"std:kv-storage";

KV Storage 模块和导入映射提案与内置模块规范密切相关。它们俩不属于 ECMAScript 规范,而属于 WICG(Web Incubator 社区组)。

KV Storage 模块:Chrome 74 增加了第一个内置模块 KV Storage。KV Storage 解决了 localStorage 的性能问题,并继承了简单 API 的优势。

  • 在 Chrome 74 中,可以使用 chrome://flags/#enable-experimental-web-platform-features 标志启用 KV Storage。

  • 请参阅“内置模块演示”页面中的 KV Storage 演示

KV Storage 具有与 Map 对象类似的 API。字符串和可序列化数据类型可用作键值。它返回一个 Promise 或 Async 迭代器,它们会被异步处理。

两个命名导出分别是“ storage ”和“ StorageArea ”。

  • storage: StorageArea 类的实例,名称为 default (默认存储数据库为“ kv-storage:default ”)。

  • StorageArea: 适用于需要额外隔离的情况(例如,存储数据并希望避免与通过默认的 storage 实例存储的数据冲突的第三方库)。 StorageArea 数据存储在名为 kv-storage:$ {name} 的 IndexedDB 数据库中,其中名称是 StorageArea 实例的名称。

复制代码

import{storage} from"std:kv-storage";
constmain =async() => {
constoldPreferences =awaitstorage.get("preferences");
document.querySelector("form")
.addEventListener("submit",async() => {
constnewPreferences =Object.assign({}, oldPreferences, {
// Updated preferences go here...
});
awaitstorage.set("preferences", newPreferences);
});
};
main();

导入映射:导入映射提案允许控制 JavaScript import 语句和 import() 表达式获取的 URL,并允许在非导入上下文中复用此映射。

导入映射为内置模块提供 Polyfill 和回落,使它们能够将当前不可用的模块标识符映射到 URL 上(参阅 https://docs.google.com/document/d/1vFQzbmxg9ilpg8CT_P8roEYcpTfZ06Q5N4J9-ZQqqZo/edit )。

例如,内置模块 KV Storage 目前仅在 Chrome 中可用。在支持的浏览器中不会出现加载问题,但对于没有提供支持的浏览器来说,就需要加载 KV Storage 的 polyfill 了。

以下示例介绍了导入映射的用法。定义模块的映射并使用 import 语句的关键 URL 值。

在没有提供支持的浏览器中,导入 URL 被识别后作为普通导入 URL 处理。如果浏览器提供了支持,它们将根据映射信息流动。

复制代码

<!-- The import map is inlined into your page -->
<scripttype="importmap">
{
"imports": {
"/path/to/kv-storage-polyfill.mjs": [
"std:kv-storage",// if has native support
"/path/to/kv-storage-polyfill.mjs"// otherwise load polyfill
]
}
}
</script>
<!-- Then any module scripts with import statements use the above map -->
<scripttype="module">
import{storage}from'/path/to/kv-storage-polyfill.mjs';
// Use `storage` ...
</script>

结语

JavaScript 仍在不断变化。事实证明它之所以成为多年来最流行的语言是有道理的。浏览器和 Node.js 对新版 ECMAScript 版本的支持也愈加完善,甚至会提前支持一些尚未完成的提案。

在这种透明而强大的标准化流程帮助下,不断进化的 JavaScript 成为了可靠而强大的语言。

前路漫漫,我们携手共进吧!

英文原文: https://medium.com/@alberto.park/status-of-javascript-ecmascript-2019-beyond-5efca6a2d233


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK