iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 4
1
Modern Web

深入現代前端開發系列 第 4

Day4 [JavaScript 基礎] 我知道 `==` 與 `===` 不同,但為什麼? 淺談相等性

在 Javascript 當中,===== 的差別時常被拿出討論。而兩者的差別相信有寫過一點 JavaScript 的都知道,== 會在比較時隱含地做型別轉換。

另外,幾件事情要注意的事是,JavaScript 的比較會因為型別不同而有所差異,例如陣列、物件(Object)、或 class,他們比較的方式是兩個變數是否指向同一個 reference。

舉例來說:

var a = [1,2,3,4]
var b = [1,2,3,4]
console.log(a === b); // ???

var objA = { name: 'JavaScript' }
var objB = { name: 'JavaScript' }
console.log(objA === objB); // ???

有經驗的工程師應該會知道這兩個的比較結果都會是 false

Reference v.s value

為什麼要設計成指向 reference,而不是他們的

首先我們需要定義什麼是相等性,在程式語言當中,相等性通常可以分為三種:

  1. Reference:兩個值指向的指標相同
  2. Shallow Compare:兩個物件的 attributes 長度以及名稱相同,且屬性的 reference 也相同。
  3. Deep Equal:兩個物件的 attibutes 長度、名稱相同,值也相同,如果有巢狀結構,會遞迴持續比較。

其中的難處在於如果不用指標來判斷相等性,程式語言不知道要怎麼幫你判斷兩個物件是否相等,要用 shallow compare 還是 deep equal,用 deep equal 來做相等性判斷可能會遇到 circular reference 的問題,所以最簡單也最有效的方式就是直接用 reference 判斷。

什麼是 circular reference 呢?就是在屬性中引用自己的 reference,程式碼大概像這樣:

let a = { name: "kalan" , age: 23, me: null };
a.me = a;

像這樣子如果用 deepEqual 做比較的話,會無限遞迴直到瀏覽器跳出 max stack size exceeds。這顯然不是個原生比較相等性的好方法,在實務上如果實作這類的 function 會再用 JSON.stringify()JSON.parse 或是設定最深只到幾層。

然而在一般應用當中,有時候我們也會希望 objA === objB 為真。

如果想要判斷相等性,在 Java 當中你會實現一個叫做 equals 的方法。在 Java 當中,因為 == 只在兩個變數指向同一個物件的時候才會回傳 true,如果只是單純比較值,就會用 equals 這個函數。

不過像一般的原始型別,就是直接用值來比較,例如字串或數字:

const a = 'hello';
const b = 'hello';
console.log(a === b); // true!

至於為什麼要這樣設計?在比較原始型別的時候,我們通常是要比較它們的值,所以在判斷上也比較單純。

不過為什麼 JavaScript 沒事要設計 3 個等號?兩個不一樣的型別做比較,照理來說應該要直接等於 false 才對,沒有什麼好爭議的。

但 JavaScript 是個動態語言,為了便利跟彈性,所以當兩個比較的型別不同時, JavaScript 會偷偷幫你做型別轉換,例如:“42” == 42, true == 1 都會回傳 true,更詳細的過程可以參考這裡

實務上來說,用兩個等號實在太容易造成混肴了,所以在實際開發上幾乎都是使用三個等號來避免型別轉換造成奇怪的問題。

如果你剛入門 JavaScript,可能會對這樣的結果相當疑惑。如果不希望 JavaScript 幫你做隱含的型別轉換,就要使用三個等號。

浮點數運算

在進行浮點數運算的時候,你可能會發現一件事:

0.1 ** 2 === 0.01 // false

0.1 的平方竟然不等於 0.01,為什麼!

這其實不算 JavaScript 本身的問題,而是電腦設計上的限制造成的。其中最主要的原因在於,電腦能夠表達的數字是有限的。

電腦是用二進位來做運算,也就是所有的數字都是用二進位來表達,像是 2 的二進制會被表示成 10,4 的二進制會表示成 100,以此類推。

小數也是如此,例如 0.25 用二進制表示是 0.01,關於二進制轉換的方式可以參考維基百科

不過為什麼電腦要用二進制而不用十進制計算?原因是對於電腦來說,這樣比較好計算。為什麼會比較好計算?因為只有 1 和 0,對於電路來講,剛好可以對應到 true 和 false,這件事是相當好用電路模擬的。

小數點後的位數每格代表往後退一位,所以換算是https://ithelp.ithome.com.tw/upload/images/20190905/20103565bra8iJlhZB.png。但是如果沒辦法剛剛好表達怎麼辦?像是 0.01,大概可以近似成 https://ithelp.ithome.com.tw/upload/images/20190905/201035650SZNpV1mZn.png(根據電腦的精度,會再更近似)。

但電腦的精度是有限的,所以只能盡量逼近這個數字,而無法完全等於。

你可能認為這件事沒什麼,不過就是記得浮點數有些地雷而已。

但一旦涉及貨幣交易或是要求高度精確性的系統時,這些浮點數的運算就不只是紙上談兵而已,會變得相當重要,把 (0.01 ** 2) * 100000,誤差也會提高 100000 倍。雖然很多時候前端只是做顯示而已,但在做運算的時候如果能夠事先注意這類的問題,可以省下不少除錯的時間。

小結

這個章節我們談論了 JavaScript 的相等性以及 ===== 的差異性以及浮點數運算為了保持一致性,在比較物件、陣列等非原始型別,會使用參考來做比較;但使用原始型別時,則會用來做比較。

因為 JavaScript 是動態語言,所以允許不同 type 可以做比較,這導致了 JavaScript 在比較時會做隱含的型別轉換,如果不希望這樣子的結果,可以使用 === 來避免。

另外,處理浮點數的時候要特別注意因為電腦能夠表達的數字有限,造成可能不相等的問題。


上一篇
Day3 [JavaScript 基礎] JavaScript 基本陣列操作
下一篇
Day5 [JavaScript 基礎] Event Loop 機制
系列文
深入現代前端開發32

尚未有邦友留言

立即登入留言