localeCompare

為什麼 Node.js 和瀏覽器中使用 localeCompare 時,排序會不同呢?

會有這篇文的誕生是因為,最近在工作上需要處理一些字元排序的功能,想要整理成筆記,但意外發現在瀏覽器中和 Node.js 使用 localeCompare 時排序卻不一樣,於是稍微探究了一下箇中原因。

const arr = [
  { id: 1, title: 'Rd 測試工程師1' },
  { id: 2, title: 'rd 測試工程師1' },
  { id: 3, title: '(Rd) 測試工程師1' },
  { id: 4, title: '[Rd] 測試工程師1' },
  { id: 5, title: '{Rd} 測試工程師1' },
  { id: 6, title: '「Rd」 測試工程師1' },
  { id: 7, title: '寶可夢工程師工程師1' },
  { id: 8, title: '測試工程師1' },
  { id: 9, title: '測試工程師2' },
  { id: 10, title: '測試工程師3' },
  { id: 11, title: '測試工程師4' },
  { id: 12, title: '測試工程師10' },
  { id: 13, title: '測試工程師12' },
  { id: 14, title: '測試工程師22' },
]

arr.sort((a, b) => a.title.localeCompare(b.title))
console.log(arr)

在瀏覽器中的 console 執行的結果:

[
  { id: 3, title: '(Rd) 測試工程師1' },
  { id: 4, title: '[Rd] 測試工程師1' },
  { id: 5, title: '{Rd} 測試工程師1' },
  { id: 6, title: '「Rd」 測試工程師1' },
  { id: 8, title: '測試工程師1' },
  { id: 12, title: '測試工程師10' },
  { id: 13, title: '測試工程師12' },
  { id: 9, title: '測試工程師2' },
  { id: 14, title: '測試工程師22' },
  { id: 10, title: '測試工程師3' },
  { id: 11, title: '測試工程師4' },
  { id: 7, title: '寶可夢工程師工程師1' },
  { id: 2, title: 'rd 測試工程師1' },
  { id: 1, title: 'Rd 測試工程師1' }
]

在 vscode 中執行 Node.js 的結果:

[
  { id: 3, title: '(Rd) 測試工程師1' },
  { id: 4, title: '[Rd] 測試工程師1' },
  { id: 5, title: '{Rd} 測試工程師1' },
  { id: 6, title: '「Rd」 測試工程師1' },
  { id: 2, title: 'rd 測試工程師1' },
  { id: 1, title: 'Rd 測試工程師1' },
  { id: 7, title: '寶可夢工程師工程師1' },
  { id: 8, title: '測試工程師1' },
  { id: 12, title: '測試工程師10' },
  { id: 13, title: '測試工程師12' },
  { id: 9, title: '測試工程師2' },
  { id: 14, title: '測試工程師22' },
  { id: 10, title: '測試工程師3' },
  { id: 11, title: '測試工程師4' }
]

1. 排查過程

1.1 查看 localeCompare 相關文件

既然矛頭出在 localeCompare 上,當然就先查詢 localeCompare 的相關文件,於是去看了一下 localeCompare 的 MDN 文件,結果發現這個 method 其實算是 Intl.Collator 的語法糖,再看了一下 Intl.Collator 的 MDN 文件,只看得出用法,並沒有說 new Intl.Collator 時,如果不給 argument 時會使用哪國的語系之類的。

根據使用 VSCode 一段時間的經驗,我想到 VSCode 有個很神奇的功能,只要按住 Command 鍵,再移到想知道定義的 function,點擊該 function,就可以看到該 fucntion 相關的說明甚至是定義,於是這次換成用 VSCode 來查看關於 localeCompare 的描述:

localeCompare
▲ 按著 Command 鍵 hover 到想看的 method 上,會出現相關描述
localeCompare
▲ 點擊該 function 後,會顯示該 function 的相關定義

可以看到這邊對 locales argument 的描述:If you omit this parameter, the default locale of the JavaScript runtime is used. 如果忽略該 argument 的話,預設的語系會是 JavaScript 執行環境的語系。

於是好像有了那麼一點頭緒!可能是因為瀏覽器與 Node.js 預設的語系不同對吧,所以下一步就來確認一下這兩種執行環境的預設語系!

1.2 查看瀏覽器與 Node.js 的 default locale

一開始要找預設的語系還真是費了不少功夫,好像比較少人會需要查詢這種東西,但摸索了一陣子還是找到 stackoverflow 上的這篇文章,完美的解答了我的問題!於是我在瀏覽器的 console 和 VSCode 的 JavaScript 檔案都執行了這段:console.log(new Intl.NumberFormat().resolvedOptions().locale),發現瀏覽器的 default locale 是 zh-TW,Node.js 則是 en-US。

這麼一來問題看似解決了,但我還滿好奇,為什麼我的 Node.js 的 default locale 是 en-US?如果是在我的電腦執行,那系統的預設語言應該也是正體中文才對。充滿好奇心的我於是打開 iTerm terminal (注意:不是在 VSCode 打開,而是另開 iTerm terminal 的視窗),查詢一下 Node.js 的全域變數。

1.3 在 Terminal 查看 Node.js 的全域變數

於是我先用 node 進到 JavaScript console,再直接 console.log(process.env) 查看全域變數:

$ node
Welcome to Node.js v14.16.1.
Type ".help" for more information.
> console.log(process.env)

不查還好,一查不得了:

localeCompare

Node.js 的全域變數明明也是正體中文呀!為什麼在 VSCode 裡的 terminal 執行卻是 en-US?於是我又查了許多文章,也在想是什麼原因讓 Node.js 在執行的時候把 default locale 的設定給覆蓋了。

1.4 查詢 Node.js 的版本及路徑

第一個想法是:可能在 iterm 裡執行的 Node.js 和 VSCode 裡的 terminal 執行版本不同,於是我在兩邊的 terminal 查詢了一下 Node.js 的路徑:

# 輸出預設使用的 Node.js 路徑
$ which node

兩邊的結果都一樣,而且我直接在兩邊的 terminal 執行同一個 JavaScript 檔,console.log(new Intl.NumberFormat().resolvedOptions().locale) 的結果仍然不同,iterm 的結果是 zh-TW,VSCode 的 terminal 則是 en-US。

再查了茫茫資料毫無頭緒時,不知道哪來的靈光乍現,不然我在 VSCode 的 termianl 裡查詢一下 Node.js 的全域變數看看好了,結果不得了!!

localeCompare

VSCode 裡查詢 Node.js 的全域變數,default locale 竟然是 en-US!

1.5 修改 VSCode 的預設語言

於是我推測可能是 VSCode 本身語言的設定影響到了 Node.js,為了印證我的推測,修改看看 VSCode 的語言設定:cmd + shift + P 後,輸入 Configure Display Language

localeCompare

選擇中文(繁體) (應該叫正體才對呢…)

localeCompare

接著重新啟動 VSCode,這時候再重新查看 Node.js 的全域變數時,LANG 就變成了 zh_TW.UTF8!沒想到罪魁禍首竟然是 VSCode,真是出乎我的意料!

2. Solution

第一個解法就像上述的,直接修改 VSCode 的語言,這樣在 VSCode 的 terminal 執行 Node.js 的 default locale 就會是你在 VSCode 的語言。

如果覺得改語言太麻煩,那就在執行 Node.js 前先修改 process.env 的變數:

$ LANG='zh_TW.UTF-8' node test.js

如此一來在執行 test.js 時,process.env.LANG 就會是 zh_TW.UTF-8。

3. 獲得的小知識

在查詢的過程中發現有些人遇到在瀏覽器和 Node.js 中使用 toLocaleString 時,雖然設定一樣的語言,卻拿到不同的顯示結果,查了一下發現,不管是在瀏覽器還是在 Node.js 中,JavaScript 處理 i18n 時都需要依賴稱為 ICU (International Component Unicode) 的 package,會不一樣是因為瀏覽器和 Node.js 支援的 ICU 完整度不同。

ICU is a mature, widely used set of C/C++ and Java libraries providing Unicode and Globalization support for software applications.
資料來源:ICU-TC Home Page

Node.js 在 13 版以前只有 small-icu mode,也就是對各國語言的支援度不完整,但在 13 版之後,已經完整支援 icu 了(full-icu),想查詢自己的 Node.js 是否使用 small-icu,可以查看 process.config.variables.icu_small,如果是 true 就代表使用的是閹割版的 icu。

至於瀏覽器的 ICU 支援度我就查不太到資料了,有頭緒的人歡迎留言給我!

參考資料

String.prototype.localeCompare() – JavaScript – MDN Web Docs
Intl.Collator – JavaScript – MDN Web Docs
JavaScript’s standard API to get the runtime’s default locale?
Visual Studio Code Display Language (Locale)
ICU Data – ICU Documentation
ICU-TC Home Page
nodejs/full-icu-npm: convenience loader for ‘small-icu’ node builds

如果覺得我的文章有幫助的話,歡迎幫我的粉專按讚哦~謝謝你!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top