下一代前端語言之爭,JavaScript 要被新語言反超?
假如大家正在編寫前端代碼,那么會選擇哪種編程語言?目前來看,最有希望的選手主要有三個:首先是最常規的 JavaScript,然后是能編譯為 WebAssembly(Wasm)的語言,最后則是能編譯成 JavaScript 的語言。
常規 JavaScript 需要的配套工具最少,但代價是調試起來相當麻煩,代碼可讀性也差。雖然選擇 JS 確實門檻較低,不過除了一味癡迷“極簡主義”的鐵粉以外,我個人覺得這個選項只能說一般。
能編譯為 Wasm 的語言雖然越來越多,但總體上還是新生事物。這些語言往往帶有大量的二進制文件,因為其中大多需要配合額外的運行時。Interop 距離發展成熟還差得遠。另外,即使兩種語言都能編譯成 Wasm,也不代表它們之間就能良好實現互操作。再有,這個陣營的生態儲備還遠遠比不上積累了幾十年的 JavaScript DOM 庫。在 Wasm 這邊,React 和 Svelte 應該是最好的選項了。大家千萬別誤會,我可不是在唱衰 Wasm。它已經擁有專屬于自己的表現舞臺,如果大家想要在瀏覽器中運行高計算量原生代碼,但 Wasm 就是最完美的選項。可如果不是這種情況,我個人不太推薦用它進行日常前端開發。
最后剩下的就是能編譯成 JavaScript 的語言了。但這個陣營形成了一家獨大的局面,其中的老大我們稍后會具體討論。相比之下,ClojureScript、Elm、ReScript、Dart 等語言都形成了頗具體量的社區,但未來市場份額還能不能進一步擴大尚未可知。這就很尷尬了,畢竟能編譯成 JavaScript 的語言代表的基本就是瀏覽器上的最佳編程體驗。在它們的支持下,我們既能享受 JS 所不具備的良好功能,比如靜態類型、強類型、不變性、宏等,同時也能通過 bindings 支持 JS 及其廣泛的生態系統。而且,它們還不需要笨拙的大型運行時。
由于 Wasm 的存在,我懷疑 JS 編譯陣營會有所保留,畢竟很多人覺得前者才是瀏覽器上的最佳編譯目標。我其實并不同意這種觀點,能編譯成 JavaScript 的語言還是越多越好。總之,我想借這篇文章跟大家聊聊現有及未來可能出現的前端語言,應該朝著哪個方向發展。
TypeScript 還行嗎?
這就是我前文提到的 JS 編譯陣營中的“老大”——TypeScript。TypeScript 是種很棒的語言,顯著改善了開發者體驗。它還新增了安全層,促進工具質量提升,并大大降低了使用門檻。考慮到生態系統的繁榮現狀以及對 JS 類型檢查難題的妥善解決,TypeScript 確實取得了非凡的成就。
當然,也有不少針對 TypeScript 的非議值得關注。首先就是這門語言的性能和健全性問題。需要注意的是,TypeScript 團隊其實很清楚這兩大頑疾,而其根源是開發團隊在項目之初做出的明確權衡。在我看來,這些權衡是當時為了提高執行效率而做出的正確選擇。
話雖如此,但性能確實是 TypeScript 最受詬病的問題。TypeScript 是自實現的,而且這種實現非常復雜。它的類型系統本身可以算是種迷你編程語言,這導致類型檢查的速度極其緩慢。
第二個問題就是健全性。這事的討論熱度沒那么高,但在編程愛好者群體內部還挺受關注。概括來講,TypeScript 一身都是“缺陷”——allwJs 配置選項、any 類型和 intersection 類型,其類型系統根本無法保證代碼的類型安全。換言之,我們編寫的 TypeScript 很可能會觸發運行時 bug。另外,除了極其簡單的場景之外,TypeScript 還缺乏可靠的類型推斷,所以開發者在很多地方都得明確標出類型注釋。
但同樣的,這兩點也是項目權衡的結果。
引導編譯器的存在對于 TypeScript 的內部測試至關重要,這能幫助項目開發者理解 TypeScript 這種語言用起來的真實感受。具體來講,項目團隊要體驗如何編寫大型 JS 代碼庫,再逐步采用代碼庫中的類型。在健全性方面放松一點,開發者才能在現有 JS 代碼庫中逐步引入 TypeScript,也能輕松使用 any 類型來直接擺脫類型系統的束縛。
光是這部分就夠單獨寫篇文章了。在我看來,TypeScript 可能是第一種更多關注開發者體驗、而非自身語義的編程語言。它并沒有添加任何運行時結構、不插手性能,而是添加了一套類型系統,并讓整個語言社區接納了這種不用類型也行、沒高質量工具也行,還不強調正確性的生態氛圍。這簡直是個不可思議的壯舉。
下一代前端語言是什么樣?
所有這一切都表明,TypeScript 早在十年前就做出了一眾對自身產生巨大影響的權衡。而隨著時間推移,我覺得是時候通過新語言再做一輪權衡了。確切來講,我們需要一種具備健全性、類型推斷和更快編譯速度的語言。
要求明確了,但我們該拿什么來換?
健全性
先從健全性說起。下一代語言不再努力對各種 JS 模式進行類型檢查,而是以獨立語言的形態通過更簡單的類型系統將代碼編譯成 JS。它會將現有 JS 代碼視頻外部互操作對象,對 JS 代碼執行顯式運行時類型檢查,而且依靠不同的原生語言來實現。
為什么要這樣?首先,我個人特別喜歡具備既健全、又相對簡單的類型系統的語言。我希望這種語言能夠在瀏覽器中運行良好,而且能順暢適配現有 Web 生態系統。那些能編譯成 Wasm 的語言經常忽略 Web 生態系統中的其余部分,總想在瀏覽器中建立起基于像素的原生 UI。我覺得這個想法不錯,只是跟我的觀念相悖。我只想用下一代語言開發常規網站;我不想要純函數式語言,而更傾向于跟 C 的老派風格相似的語言(對不起了,Elm!);我希望這種語言能體現出我在工具設計上的想法。
那為什么下一代前端語言應該誕生在現在這個時間點?俗話說得好,種一棵樹最好的時機是十年前,其次是現在。這十年來,JS 社區已經發生了很大變化。人們開始學習 TypeScript,也習慣于關注編譯器并通過類型進行數據建模。現在,很多開發者開始使用 Rust、Swift 和 Kotlin 等語言,也意識到高質量工具的重要性。我不是說十年前的人們會抵抗強調類型安全的語言,但那時候的普及難度確實更高。
明確表達了需求,有些朋友可能覺得這說的不就是 ReScript/ReasonML 嗎?沒錯,確實有幾分相像。但在理想情況下,我期待的下一代語言應該能對 JS 代碼和特性進行顯式運行時類型檢查。運行時類型檢查是達成良好互操作性的前提,這樣我們就能更輕松地隨意使用 JS 庫。
同樣地,我覺得 traits 對用戶來說也很重要,它們可以跟其他語言特性映射起來,比如 Java 接口和 C++ 概念。這可太方便了,比如輕松通過 Display trait 輸出任意類型。這類需求聽起來簡單,但確實能大大提升語言的可用性,消除“我該怎么輸出這個?”或者“為什么 + 代表整數加法,而 +. 代表浮點加法?”之類特別勸退的問題。再有,我還想去掉一些沒用的東西,比如對象、鏈表、多態變體等。這些都是 ReScript/ReasonML 做不到的,而且我上次試用的時候,ReScript 的開發體驗和錯誤消息也沒給我留下深刻印象。
也就是說,我不排除 ReScript 代表著正確方向的可能性。畢竟上次嘗試已經是幾年之前了,也許是我記錯了、也許它已經變得更好了。而且隨著同 OCaml 的剝離,ReScript 確實成了很好的前端語言選項,我有必要再確認一下。
類型安全
對于下一代前端語言,我希望能用一種更系統的方法實現類型安全。具體來說,我覺得用 Rust 處理非安全代碼塊的方式實現 JS 互操作性的好辦法。基本上,在調用 JS 的過程中,我們需要將代碼打包在一個非安全代碼塊中。這會是個明確的標志,提醒開發者要認真閱讀這段代碼。接下來的目標,就是在這些指向 JS 庫的非安全代碼塊上實現 bindings。起初這個過程需要手動完成,但后續應該會有類似 bindgen 和 cxx 的工具出現。
在 JS 中使用非安全代碼塊好像有點反直覺,畢竟 JS 的安全性又不像 C 那么糟糕。但很多人似乎沒意識到,安全的意義并不僅限于安全本身。所謂安全,是指可以任意使用一個值、而不必擔心其是否為 null 的保障能力。所謂安全,是在不致引入 Bug 或混亂的前提下保證可變性的能力。Rust 的非安全塊概念允許用戶既維護自己的安全區,又能與大量非安全代碼交互。下一代瀏覽器語言也該做到這一點。
至于運行時檢查,我覺得它仍然物有所值。我們已經在 JS 當中進行過大量模式驗證,只是以往只能通過 zod 這類臨時性機制完成。在下一代前端語言中,這類功能也許是在運行時出錯時對語言類型執行自動轉換,也許能對 JS 值進行模式匹配。
對于 WebAssembly,我還是很看好它的發展前景的。但要說它一定能成為瀏覽器的通用運行時,我個人還是持懷疑態度。也許未來我的態度會有轉變,但目前我更多是將 Wasm 看作一種硬件加速器。
當用戶的高強度計算任務要求調用固定寬度整數和靜態函數時,大家就會使用 Wasm;這就像在需要執行并行計算時,大家會選擇 GPU 一樣。在這樣的模型中,我看到了支持異構編譯的潛力——其中部分代碼可以被編譯成 JS,另一部分代碼則可編譯為 Wasm。這項工作可以由用戶顯式完成,由分析自動完成,甚至可以即時完成。通過對 JS 和 Wasm 代碼的同時控制,編譯器就能最大限度減少跨越語言邊界的次數,從而提高性能水平。我覺得未來甚至可以有某種機制將部分代碼發送給 WebGPU。
在這樣的模型之上,也許我們可以更輕松地編寫計算密集型程序,比如機器學習模型、電子游戲和渲染軟件。
這種對 Wasm 和 JS 進行分別編譯的概念,可以在下一代前端語言中體現出來。我希望其中能有顯式整數和浮點類型,最好還能有 Rust 中 usize 那樣的顯式索引類型。這樣如果需要把代碼編譯成 Wasm,新語言就能利用 Wasm 的固定寬度整數。
還有另一種可能性,就是為語言創建一個子集,在這里整合閉包、垃圾收集等動態特性以提升 Wasm 編譯質量。要跟這個子集交互,開發者需要使用 unsafe 代碼塊,比如 strict 塊,或者讓該子集通過 dynamic 塊跟外部代碼交互。這些都是假設,但我覺得其中確有探究的價值。
具體實現
這種新語言可能會用 Rust 來實現。畢竟我個人是 Rust 的粉絲,而且相信代數數據類型、相對更高的代碼性能、受限但可用的可變性,以及比較豐富的庫組合足以支撐起一套優秀的編譯器。
如果 Wasm 后續發展得夠好、性能幾乎逼近原生水平,那我也會考慮使用由編譯為高速 Wasm 代碼的語言子集來引導編譯器。但這應該不著急,畢竟一個 Rust 編譯器應該就夠用好多年了。
總 結
大家可能已經注意到,類型安全和 Wasm 部分其實就是在從系統語言(例如非安全概念和硬件加速)中汲取靈感,再把它們應用到基于瀏覽器的語言當中。這是設計使然,畢竟不少最有趣的編程語言都是從系統層面衍生出來的。我只希望這些好點子也能在瀏覽器上有所體現。
這里我要澄清一下,我所指的下一代前端語言絕不是單一語言,我希望能有多種語言齊頭并進、朝著前面提到的方向共同探索。我想激勵更多朋友在瀏覽器語言領域不斷創新。當然,我個人也會參與其中,目前正在研究的是名叫 vicuna 的實現方案,但還處于非常早期的階段。