正式釋出的ES(ECMAScript)11中,多了bigint基本型態,用來解決JavaScript開發中對大整數的需求,許多文件都會談到:這個特性普及後,可免除透過第三方程式庫時下載與運行效率等方面的負擔。雖然免除第三方程式庫確實是相關的效益,然而,並不是完全正確的說法。

理由在於「bigint是個基本型態」,一門語言在主要特性奠定,有了廣大使用者後,越是構築語言基礎的元素,除非有絕對充分的理由,否則越難對其變動或新增,這單純只是第三方程式庫相關問題,真的足以構成JavaScript增加一個基本型態的理由?

增加了基本型態?

可能有開發者會說,JavaScript增加基本型態奇怪嗎?之前不就增加了symbol嗎?增加symbol之目的,主要是為了讓過去隱式的物件協定,能有個正式、明確的做法,也就是說,物件協定的概念早就在過去JavaScript之中,只不過以前都模糊地、非正式地使用特定的名稱、借用了字串型態來表示。

另一方面,雖然過去會使用字串來表示物件協定時的名稱,這類字串與其他基本型態結合運算的場合比較單純,大概就是搜尋、組合、剖析名稱這類需求,遇上這類需求時,可以透過symbol與字串間的轉換來銜接,相關的使用場景可以單純化。

然而,bigint代表了大整數,在還沒有bigint前,JavaScript只有number,實現了IEEE 754標準64位元浮點數,也就是說,過去JavaScript並沒有真正的整數型態;另一方面,整數就是數字,而數字運算需求一直都有,新增的數字型態與既有的number型態,該怎麼互動,其實要考量的問題,遠比一些開發者想的要來得多而複雜。

number的整數問題

想瞭解JavaScript為何要增加bigint,首先就要知道number有什麼問題。的確開發者可以在JavaScript撰寫整數,但number骨子裡還是浮點數,IEEE 754標準的浮點數,分為符號(sign bit)、指數偏差(exponent bias)與分數(fraction),對於64位元浮點數,三部份佔的位元數,各是1、11與52個位元。

也就是說,如果number只用來表示整數,位元組中可使用的範圍就是52個位元的分數部份,最大整數只能表示到2**53-1(253-1),也就是Number.MAX_SAFE_INTEGER的值9007199254740992,若以Java的long最大值9223372036854775807來比較,實在是少太多了,那麼,使用第三方程式庫來表示更大的整數,不就好了嗎?

若面對的只是數字計算,實現大整數運算的第三方程式庫或許能解決,但問題往往不那麼單純。例如看看前後端資料傳遞的問題,許多文件常提的案例就是Twitter的64位元ID,或是高精度時間戳記,前後端溝通時,須以字串方式傳遞、轉換處理。

另外,在面對WebAssembly、WebGL時,會有64位元整數的整合問題,例如,WebAssembly支援i64,若要與JavaScript互通,參數或傳回值就必須合法化(Legalization),參數部份要將i64改為兩個i32,分別表示64位元整數的高低位元組,i64傳回值的部份也必須想辦法拆成兩部份,對這些有興趣的話,可以進一步參考〈WebAssembly integration with JavaScript BigInt〉。

另外,還要留意位元運算的問題,若對JavaScript的number進行位元運算,數字會被當成32位元整數,因此2**32-1>>0的最高位元是1,也就是2補數表示整數-1;簡單來說,問題並不只有大整數運算,有興趣可以進一步參考〈Bigints (advanced)〉。

bigint與number混合運算?

無論如何,bigint作為基本資料型態加入JavaScript是既成事實,若想知道使用方式,網路上已經有非常多的文件介紹了,然而,在觀看相關介紹時,我們可能會產生一些困惑,為什麼bigint與number如此格格不入?它們不都是基本型態的數字嗎?為什麼不能混合運算?為什麼會拋出TypeError?為什麼設計上不使用兩個型態,分別代表整數與浮點數呢?

在bigint的提案中〈Design Goals〉談到幾個影響設計決策的因素,第一就是在精度與使用者直覺間找平衡點,有人會說:「混合運算時拋出TypeError,不就顯然違反使用者直覺嗎?」是的,因為拉扯的點在於精度的考量!

舉例來說,如果bigint與number可以混合運算,你覺得9007199254740993n+0.1應該是什麼型態?bigint?那麼,小數部份怎麼辦?number?那會有精度的問題,數學上的計算結果,應該是9007199254740993.1,然而,用number表示,結果會是9.007199254740994E15。

有人說,反正開發者早就習慣面對IEEE 754精度問題了,他寧可用精度問題來換取直覺,而非拋出TypeError,問題是:你又要犧牲哪個方向的精度?

其實,在bigint之外,有過另一個隱式整數的提案,該提案將number作為Int與Double的父型態,如果撰寫整數實字就預設為Int,若撰寫浮點數就預設為Double,當中也定義了混合運算的規則,有些開發者認為這麼做更符合直覺,也可以解決大整數的問題。

但該提案最大的問題是,抵觸〈Design Goals〉中的幾個Don't break原則。

例如,若採用該原則,9007199254740992+1結果要是9007199254740993,然而JavaScript沒有大整數方案前,結果一直都是9007199254740992,其他還有像是,基於現況實作位元運算的一些應用程式,也會遭到破壞掉等的問題。

因此,結果就是bigint成為最終提案了。既然bigint無法與number混合運算,試圖這麼做會拋出TypeError,讓使用者自行決定該怎麼處理兩種型態的轉換問題。不過,這也衍生了幾個問題了,例如平方根2**0.5這種事無法用bigint做到,因為2n**0.5就是bigint與number混合運算了,當然也沒有2n**0.5n這種寫法,畢竟0.5是小數,Math上的函式接受的number,不能使用bigint。

因此搭配bigint的程式庫還是必須存在的,無論是標準或是第三方,例如Math上一些函式,就必須有bigint的版本,事實上,就有個〈BigInt Math for JavaScript〉被提案了。

規格書成形的過程

每當看到語言有個新特性時,無論是新API、新語法,除了看看一些入門的介紹與使用方式之外,我總是會想看看規格書提案或與其相關的討論,加入語言的特性越是基礎,越需要如此,從那些規格書提案或相關討論,可以不斷地思考加入的理由是什麼?既有的方案是什麼?社群的想法與反應?規格書考量了什麼?為什麼如此設計?

JavaScript加入了bigint,我想到的第一件事是「這可是基本型態!」、「為什麼不是處理大數API規格書呢?」等問題,而從這個方向出發,我找到了許多有趣的文件與討論,除了bigint規格書與方才提及的隱式整數提案以外,bigint規格書的ISSUE#30#36,都值得看看!

記住,加入越基礎的新特性,越需要更多的考量,而這些考量是絕對必須探討的,道理很簡單,在已蓋好的地基上搭建高樓了,現在卻要往地基裡加個什麼,聲稱其很重要,一定要有很好的理由才能這麼做吧!這麼做之後又要付出哪些代價?必須有哪些配套措施等,難道不值得好好思考一番嗎?

專欄作者

熱門新聞

Advertisement