幾年后的 JavaScript 會是什么樣子?
轉自:code秘密花園
大廠技術??高級前端??Node進階
點擊上方?程序員成長指北,關注公眾號
回復1,加入高級Node交流群
前言
最近看到了一些很有趣的 ECMAScript 提案,如 Record 與 Tuple 數據類型,借鑒自 RxJS 的Observable,借鑒自函數式編程的 throw Expressions,帶來更好錯誤處理能力的 Error Cause 等??梢哉J為,等這些提案推進完畢,進入到 未來的 ES Next 特性中,JavaScript又會迎來一次變革。而這篇文章將列舉一部分筆者認為值得關注的 ECMAScript 提案,既包括新的API(如先前的replaceAll),也有著新的語法(如先前的?.與??等),目前它們中的大部分仍停留在 stage1/2 中,因此想要實際使用還需要使用借助 Babel 插件,或是靜靜等待...
作為前端同學,即使你沒有去主動了解過,應該也或多或少聽說過ECMA、ECMAScript、TC39、ES6這些詞,你可能對這些名詞代表的概念一知半解甚至是從未了解過,這是非常正常的,因為不知道這些名詞的意義并不影響你將各種 ES 新特性用的如臂使指。但了解一下也花不了多少時間?更是可以讓你感受到 JavaScript 這門從出生開始就被吐槽的語言其實也在努力變得更好。所以在開始正式介紹各種提案前,我們有必要先了解一下這些概念。
以下關于背景的介紹大部分來自于雪碧老師的JavaScript20年-創(chuàng)立標準一節(jié)。
ECMA(European Computer Manufacturers Association,歐洲計算機制造商協會),這是一個國際組織,主要負責維護各種計算機的相關標準。我們都知道 JavaScript 這門語言最早來自于網景公司(Netscape),但網景在瀏覽器市場上和微軟(IE)的競爭落得下風,為了避免最終 Web 腳本的主導權落入微軟手中,網景開始尋求 ECMA 組織的幫助,來推動 JavaScript 的標準化。 在1996年,JavaScript正式加入了 ECMA 大家庭,我們后面會叫它 ECMAScript(下文簡稱ES)。TC39則是 ECMA 為 ES 專門組織的技術委員會(Technical Committee),39這個數字則是因為 ECMA 使用數字來標記旗下的技術委員會。TC39的成員由各個主流瀏覽器廠商的代表構成(因為畢竟最后還要這些人實現嘛)。 ECMA-262即為 ECMA 組織維護的第262條標準,這一標準是在不斷演進的,如現在是2020年6月發(fā)布的第11版。同樣的,目前最為熟知的是2015年發(fā)布的ES6。你還可以在 TC39的ECMA262官網 上看到 ES2022 的最新草案。 ECMA還維護著許多其他方面的標準,如ECMA-414,定義了一組 ES 規(guī)范套件的標準;ECMA-404,定義了 JSON 數據交換的語法;甚至還有120mm DVD 的標準:ECMA267。 對于一個提案從提出到最后被納入ES新特性,TC39的規(guī)范中有五步要走: stage0(strawman),任何TC39的成員都可以提交。 stage1(proposal),進入此階段就意味著這一提案被認為是正式的了,需要對此提案的場景與API進行詳盡的描述。 stage2(draft),演進到這一階段的提案如果能最終進入到標準,那么在之后的階段都不會有太大的變化,因為理論上只接受增量修改。 state3(candidate),這一階段的提案只有在遇到了重大問題才會修改,規(guī)范文檔需要被全面的完成。 state4(finished),這一階段的提案將會被納入到ES每年發(fā)布的規(guī)范之中。 有興趣的同學可以閱讀 The TC39 process for ECMAScript features 了解更多。
Record & Tuple(stage2)
proposal-record-tuple 這一提案為 JavaScript 新增了兩種數據結構:Record(類似于對象) 和 Tuple(類似于數組),它們的共同點是都是不可變的(Immutable),同時成員只能是原始類型,以及同樣不可變的 Record 和 Tuple 。正因為它們的成員不能包含引用類型,所以它們是 按值比較 的,成員完全一致的 Record 和 Tuple 如果進行比較,會被認為是相同的(即'==='會返回true)。
你可能會想到社區(qū)其實對于數據不可變已經有不少方案了,如 ImmutableJS 與 Immer,它還是React中的重要概念。
使用示例:
//?Record
const?proposal?=?#{
??id:?1234,
??title:?"Record?&?Tuple?proposal",
??contents:?`...`,
??keywords:?#["ecma",?"tc39",?"proposal",?"record",?"tuple"],
};
//?Tuple
const?measures?=?#[42,?12,?67,?"measure?error:?foo?happened"];
.at() Relative Indexing Method (stage 3)
proposal-relative-indexing-method提案引入了新的APIat()方法,用于獲取可索引類(如Array, String, TypedArray)上指定位置的成員。
在過去 JavaScript 中一直缺乏負索引相關的支持,比如獲取數組的最后一個成員需要使用arr[arr.length-1],而無法使用arr[-1]。這主要是因為 JavaScript 中[]語法可以對所有對象使用,所以arr[-1]返回的是 key 為-1的屬性值,而非索引為 -1(即從后往前排序)的數組成員。
而要獲取數組的倒數第 N 個成員,通常使用的方法是arr[arr.length - N],或者arr.slice(-N)[0],兩種方法都有各自的缺陷,因此 at() 就來救場了。
另外,還存在獲取數組最后一個成員的提案,proposal-array-last (stage1)與獲取數組最后一個符合條件的成員的提案 proposal-array-find-from-last。
Temporal (stage 3)
proposal-temporal主要是為了提供標準化的日期與時間API,這一提案引入了一個全局的命名空間 Temporal(類似于Math、Promise)來引入一系列現代化的日期API(JavaScript 的 Date API 誰用誰知道,也難怪社區(qū)那么多日期處理庫了),如:
Temporal.Instant?獲取一個固定的時間對象:const?instant?=?Temporal.Instant.from('1969-07-20T20:17Z');
instant.toString();?//?=>?'1969-07-20T20:17:00Z'
instant.epochMilliseconds;?//?=>?-14182980000Temporal.PlainDate獲取calendar date:const?date?=?Temporal.PlainDate.from({?year:?2006,?month:?8,?day:?24?});?//?=>?2006-08-24
date.year;?//?=>?2006
date.inLeapYear;?//?=>?false
date.toString();?//?=>?'2006-08-24'Temporal.PlainTime獲取wall-clock time:const?time?=?Temporal.PlainTime.from({
??hour:?19,
??minute:?39,
??second:?9,
??millisecond:?68,
??microsecond:?346,
??nanosecond:?205
});?//?=>?19:39:09.068346205
time.second;?//?=>?9
time.toString();?//?=>?'19:39:09.068346205'Temporal.Duration獲取一段時間長度,用于比較時間有奇效const?duration?=?Temporal.Duration.from({
??hours:?130,
??minutes:?20
});
duration.total({?unit:?'second'?});?//?=>?469200
更多細節(jié)參考ts39-proposal-temporal docs。
Private Methods (stage 3)
private-methods 提案為 JavaScript Class 引入了私有的屬性、方法以及getter/setter,不同于 TypeScript 中使用 private 語法,這一提案使用#語法來標識私有成員,在阮老師的ES6標準入門中也提到了這一提案。
所以這個提案已經過了多少年了...
參考阮老師給的例子:
class?IncreasingCounter?{
??#count?=?0;
??get?value()?{
????console.log('Getting?the?current?value!');
????return?this.#count;
??}
??increment()?{
????this.#count++;
??}
}
類似的,還有一個同樣處于 stage3 的提案proposal-class-fields引入了static關鍵字。
對 TypeScript 使用者來說可能沒什么感覺,因為在 JavaScript 中寫 Class 越來越少了。但是這一提案成功被引入后,可能會使得TS到JS的編譯產物變化,即直接使用JS自身的static、#語法。比如現在這么一段TS代碼:
class?A?{
?static?x1?=?8;
}
編譯結果是:
"use?strict";
class?A?{}
A.x1?=?8;
而在static被引入后,則可能會直接使用static語法。
同樣的,現在的編譯結果其實并不能確保“私有”,即編譯后的 JS 代碼仍然能非法的獲取私有成員,因為從語言層面并沒有相關能力提供。
Top-level await (stage4)
proposal-top-level-await這個提案感覺就沒有啥展開描述的必要了,很多人應該已經用上了。簡單地說,就是你的 await 語法不再和 async 強綁定了,你可以直接在應用的最頂層使用 await 語法而不再需要套一個 async 函數,NodeJS也從 14.8 開始支持了這一提案。
Import Assertions (stage 3)
proposal-import-assertions 這一提案為導入語句新增了用于標識模塊類型的斷言語句,語法如下:
import?json?from?"./foo.json"?assert?{?type:?"json"?};import("foo.json",?{?assert:?{?type:?"json"?}?});
注意,對 JSON 模塊的導入最開始屬于這一提案的一部分,后續(xù)被獨立出來作為一個單獨的提案:proposal-json-modules。
這一提案最初起源于為了在 JavaScript 中更便捷的導入 JSON 模塊,后續(xù)出于安全性考慮加上了 import assertions 來作為導入不可執(zhí)行模塊的必須條件。
這一提案同樣解決了模塊類型與其 MIME 類型不符的情況,并且,和現在如火如荼的基于 ESM 的 Bundleless 工具結合應該會有奇妙的化學反應,期待 Vite3.0 能安排上這個提案。
Error Cause (stage 3)
proposal-error-cause這一提案目前由淘系技術部的昭朗同學在推進,其目的主要是為了便捷的傳遞導致錯誤的原因,如果不使用這個模塊,想要清晰的跨越多個調用棧傳遞錯誤上下文信息,通常要這么做:
async?function?getSolution()?{??const?rawResource?=?await?fetch('//domain/resource-a')????.catch(err?=>?{??????//?想想要怎么拋出一個攜帶有效信息的錯誤????//?直接拋出個異常???????// 1. throw new Error('Download raw resource failed:?'?+ err.message);???//??實例化一個error再指定屬性???????// 2. const wrapErr = new Error('Download raw resource failed');??????//??? wrapErr.cause = err;??????//??? throw wrapErr;???//?創(chuàng)建Error的子類???????// 3. class CustomError extends Error {??????//????? constructor(msg, cause)?{??????//??????? super(msg);??????//??????? this.cause = cause;??????//??????}??????//????}??????//??? throw new CustomError('Download raw resource failed', err);????})? const jobResult = doComputationalHeavyJob(rawResource);? await fetch('//domain/upload', { method:?'POST', body: jobResult });}await doJob();?//?=> TypeError: Failed to fetch
看起來好像都不太直觀?而按照這一提案的語法:
async?function?doJob()?{??const?rawResource?=?await?fetch('//domain/resource-a')????.catch(err?=>?{??????throw?new?Error('Download?raw?resource?failed',?{?cause:?err?});????});??const?jobResult?=?doComputationalHeavyJob(rawResource);??await?fetch('//domain/upload',?{?method:?'POST',?body:?jobResult?})????.catch(err?=>?{??????throw?new?Error('Upload?job?result?failed',?{?cause:?err?});????});}
Decorators (stage 2)
proposal-decorators這一提案...,或許是我們最熟悉的老朋友了。但是此裝飾器非彼裝飾器,歷時五年來裝飾器提案已經走到了第三版,仍然卡在 stage 2。
來看看它現在的實現是這樣的,是不是感覺有點詭異?

這里引用筆者早前的一篇文章來簡單講述下裝飾器的歷史:
首先我們需要知道,JS 與 TS 中的裝飾器不是一回事,JS 中的裝飾器目前依然停留在 stage 2 階段,并且目前版本的草案與 TS 中的實現差異相當之大(TS 是基于第一版,JS 目前已經第三版了),所以二者最終的裝飾器實現必然有非常大的差異。
其次,裝飾器不是 TS 所提供的特性(如類型、接口),而是 TS 實現的 ECMAScript 提案(就像類的私有成員一樣)。TS 實際上只會對 stage-3 以上的語言提供支持,比如 TS3.7.5 引入了可選鏈(Optional chaining)與空值合并(Nullish-Coalescing)。而當 TS 引入裝飾器時(大約在 15 年左右),JS 中的裝飾器依然處于 stage-1 階段。其原因是 TS 團隊與 Angular 團隊 PY 成功了,Ng 團隊不再維護 AtScript,而 TS 引入了注解語法(Annotation)及相關特性。
但是并不需要擔心,即使裝飾器永遠到達不了 stage-3/4 階段,它也不會消失的。有相當多的框架都是裝飾器的重度用戶,如
Angular、NestJS、MidwayJS等。對于裝飾器的實現與編譯結果會始終保留,就像JSX一樣。如果你對它的歷史與發(fā)展方向有興趣,可以讀一讀 是否應該在 production 里使用 typescript 的 decorator?(賀師俊賀老的回答)
和類的私有成員、靜態(tài)成員提案一樣,目前使用最廣泛的還是 TS 中的裝飾器,但是二者的語義完全不同,因此原生裝飾器的提案不太可能會影響 TypeScript 到 JavaScript 的編譯結果。
Iterator Helpers (stage 2)
proposal-iterator-helpers提案為ES中的Iterator使用與消費引入了一批新的接口,雖然實際上,如 Lodash 與 Itertools(思路來自于Python3中的 itertools)這樣的工具庫已經提供了絕大部分能力,如 filter、filterMap 等。其他語言如 Rust、C# 中也內置了非常強大的Iterator Helpers,見Prior Art。
示例:
function*?naturals()?{??let?i?=?0;??while?(true)?{????yield?i;????i?+=?1;??}}const?evens?=?naturals()??.filter((n)?=>?n?%?2?===?0);for?(const?even?of?evens)?{??console.log(even,?'is?an?even?number');}
雖然目前很少會直接和 Generator、Iterator 打交道了,但這些畢竟是語言底部的東西,了解其基本使用與運行機制還是有好處的。
throw Expressions (stage 2)
proposal-throw-expressions這一提案主要提供了 const x = throw new Error() 這一語法,但這并不是 throw 語法的替代品,更像是 **面向表達式(Expression-Oriented) ** 能力的補齊。
function?getEncoder(encoding)?{??const?encoder?=?encoding?===?"utf8"???new?UTF8Encoder()?????????????????:?encoding?===?"utf16le"???new?UTF16Encoder(false)?????????????????:?encoding?===?"utf16be"???new?UTF16Encoder(true)?????????????????:?throw?new?Error("Unsupported?encoding");}
Upsert(Map.prototype.emplace) (stage 2)
proposal-upsert這一提案為 Map 引入了 emplace 方法,在當前 Map 上的key已存在時,執(zhí)行更新操作,否則執(zhí)行創(chuàng)建操作。確實是很甜的語法糖,感覺底層框架、工具庫用 Map 多一些。
Observable (stage 1)
proposal-observable這一提案,其實懂的同學看到 Observable 這個詞已經懂這個提案是干啥的了,它引入了 RxJS 中的Observable、Observer(同樣是next/error/complete/start)、Subscriber(next/error/complete)等概念,當然還有 Operators(但目前只是部分),同樣支持高階Observable,以及在被訂閱時才會開始推送數據(Lazy-Data-Emitting)。
function?listen(element,?eventName)?{????return?new?Observable(observer?=>?{????????//?Create?an?event?handler?which?sends?data?to?the?sink????????let?handler?=?event?=>?observer.next(event);????????//?Attach?the?event?handler????????element.addEventListener(eventName,?handler,?true);????????//?Return?a?cleanup?function?which?will?cancel?the?event?stream????????return?()?=>?{????????????//?Detach?the?event?handler?from?the?element????????????element.removeEventListener(eventName,?handler,?true);????????};????});}
估計是因為還在 stage1 的關系,目前支持的操作符只有of、from這種最基礎的,但按照這個趨勢下去RxJS中的大部分操作符都會被吸收過來。個人感覺需要非常久的時間才能看到未來結果,因為 RxJS 擁有的海量操作符可不是說著玩的,如果全都要吸收過來可能會吃力不討好的。同時,RxJS的學習成本還是有的,我不認為大家會因為它被吸收到JS語言原生就會紛紛開始學習相關概念。
Promise.try (stage 1)
proposal-promise-try提案引入了 Promise.try 方法,這一方法其實很早就在bluebird中提供了,其使用方式如下:
function?getUserNameById(id)?{????return?Promise.try(function()?{????????if?(typeof?id?!==?"number")?{????????????throw?new?Error("id?must?be?a?number");????????}????????return?db.getUserById(id);????}).then((user)=>{????????return?user.name});}
Promise.try方法返回一個promise實例,如果方法內部拋出了錯誤,則會走到.catch方法。上面的例子如果正常來寫,通常會這么寫:
function?getUserNameById(id)?{????return?db.getUserById(id).then(function(user)?{????????return?user.name;????});}
看起來好像沒什么區(qū)別,但仔細想想,假設下面一個例子中,id是錯誤的,
db.getUserById(id)返回了空值,那么這樣 user.name 無法獲取,將會走.catch,但如果不返回空值而是拋出一個同步錯誤呢?Promises的錯誤捕獲功能的工作原理是所有同步代碼都位于 .then 中,這樣它就可以將其包裝在一個巨大的 try/catch 塊中(所以同步錯誤都能走到 .catch 中)。但是在這個例子中,db.getUserById(id)并非位于 .then 語句中,這就導致了這里的同步錯誤無法被捕獲。簡單的說,如果僅使用.then,只有第一次異步操作后的同步錯誤會被捕獲。
而是用Promise.try,它將捕獲 db.getUserById(id) 中的同步錯誤(就像 .then 一樣,區(qū)別主要在try不需要前面跟著一個promise實例),這樣子所有同步錯誤就都能被捕獲了。
Do Expression (stage 1)
proposal-do-expressions這個提案和 throw Expressions 一樣,都是面向表達式(Expression-Oriented)的語法,函數式編程的重要特性之一。
看看示例代碼:
let?x?=?do?{??let?tmp?=?f();??tmp?*?tmp?+?1};let?y?=?do?{??if?(foo())?{?f()?}??else?if?(bar())?{?g()?}??else?{?h()?}};
對于像筆者一樣沒接觸過函數式編程的同學,這種語法可能確實很新奇有趣,但提案中還存在著一些注意點:
在 do {}中不能僅有聲明語句,或者是缺少 else 的if,以及循環(huán)。空白的 do {}語句效果等同于void 0。await/yield標識繼承自上下文
對于異步版本的do expression,存在一個尚未進入的提案proposal-async-do-expressions,旨在允許使用async do {}的語法,如:
//?at?the?top?level?of?a?scriptasync?do?{??await?readFile('in.txt');??let?query?=?await?ask('???');??//?etc}
Pipeline Operator (stage 1)
目前star最多的提案,似乎沒有之一?
proposal-pipeline-operator提案引入了新的操作符|>,目前對于具體實現細節(jié)存在兩個不同的競爭提案。這一語法糖的主要目的是大大提升函數調用的可讀性,如 doubleNumber(number) 會變?yōu)?number |> doubleNumber 的形式,對于鏈式的連續(xù)函數調用更是有奇效,如:
function?doubleSay?(str)?{??return?str?+?",?"?+?str;}function?capitalize?(str)?{??return?str[0].toUpperCase()?+?str.substring(1);}function?exclaim?(str)?{??return?str?+?'!';}
在管道操作符下,變?yōu)槿缦滦问剑?/p>
let?result?=?exclaim(capitalize(doubleSay("hello")));result?//=>?"Hello,?hello!"let?result?=?"hello"??|>?doubleSay??|>?capitalize??|>?exclaim;result?//=>?"Hello,?hello!"
確實大大提高了不少可讀性對吧?你可能會想,上面都是單個入參,那多個呢,如下圖示例:
function?double?(x)?{?return?x?+?x;?}function?add?(x,?y)?{?return?x?+?y;?}function?boundScore?(min,?max,?score)?{??return?Math.max(min,?Math.min(max,?score));}let?person?=?{?score:?25?};let?newScore?=?person.score??|>?double??|>?(_?=>?add(7,?_))??|>?(_?=>?boundScore(0,?100,?_));newScore?//=>?57
等同于
let?newScore?=?boundScore(?0,?100,?add(7,?double(person.score))?)
_只是形參名稱,你可以使用任意的形參名稱。
Partial Application Syntax(stage 1)
proposal-partial-application這一提案引入了新的柯里化方式,即原本我們使用 bind 方法來預先固定一個函數的部分參數,得到一個高階函數:
function?add(x,?y)?{?return?x?+?y;?}const?addOne?=?add.bind(null,?1);addOne(2);?//?3const?addTen?=?x?=>?add(x,?10);addTen(2);?//?12
使用Partial Application Syntax,寫法會是這樣的:
const?addOne?=?add(1,??);?addOne(2);?//?3const?addTen?=?add(?,?10);?addTen(2);?//?12
我們上一個列舉的提案proposal-pipeline-operator,其實可以在 Partial Application Syntax 的幫助下變得更加便捷,尤其是在多參數情況下:
let?person?=?{?score:?25?};let?newScore?=?person.score??|>?double??|>?add(7,??)??|>?boundScore(0,?100,??);
目前的實現暫時不支持 await。關于更多細節(jié),參考 Pipeline operator: Seeking champions 以及 Pipeline operator draft。
await.opts (stage 1)
proposal-await.ops這一提案為 await 引入了await.all/race/allSettled/any四個方法,來簡化 Promise 的使用。實際上它們也正是Promise.all/race/allSettled/any的替代者,如:
//?beforeawait?Promise.all(users.map(async?x?=>?fetchProfile(x.id)))//?afterawait.all?users.map(async?x?=>?fetchProfile(x.id))
以上只是一部分有趣的、筆者認為值得關注并且很有可能走到最后的提案,如果你有興趣了解更多,不妨前往TC39 Proposals Tracking,了解更多處在stage1/2/3的提案。
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。

???“分享、點贊、在看” 支持一波??
