iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
3
Modern Web

你懂 JavaScript 嗎?系列 第 6

你懂 JavaScript 嗎?#6 值(Values)Part 2 - 特殊值

你所不知道的 JS

本文主要內容為探討基本型別的特殊值並能適當地使用它們。

undefined 與 void 運算子

void 運算子可確保運算式不回傳任何值(其實是得到 undefined),並且不修改現有值。

例如,這個有一個變數 hello,其值為 777,結合 void 運算子做運算後會得到 undefined,但 hello 內儲存的值仍是不變的,依舊是 777。

var hello = 777;

void hello // undefined
hello // 777

實際上會應用到什麼狀況呢?

一,運算式的結果真的希望 不回傳任何值(再次強調,其實是回傳 undefined),除了直接寫「undefined」外,還可以用「void 某個值」,通常會用「void 0」。

function sayHi() {
  return void 0;
}

const result = sayHi()
result // undefined

二,在程式設定下,必須區別有意義和無意義的回傳值,而無意義的回傳值希望能是 undefined,以避免後續誤判為「有意義」的回傳值而做了錯誤的操作。如下範例所示,這裡有一個定期檢查回傳結果的函式 check,check 會呼叫函式 getResult 來得到運算結果並確認結果為何,若沒有得到結果,就顯示經過的分鐘數;若得到結果就印出「工作完成」的訊息。在這裡無意義的回傳值是使用 undefined,但當然很多開發者是比較喜歡用 false 或 null,就看當時的需求和個人喜好摟。

const interval = 60000;
let start = null;
let counter = 1;

// 經由一些運算得到結果 result,若有結果則 flag "isDone" 設為 true 並回傳結果;若無結果則 flag "isDone" 設為 false 並回傳 undefined
function getResult() {
  if (isDone) {
    return result;
  }
  return void 0; // 等同於 undefined
}

// 不斷重複詢問是否得到結果?若沒有得到結果,就顯示經過的分鐘數;若得到結果就印出「工作完成」的訊息
function check(timestamp) {
  const progress = timestamp - start;
  if (start === null) { start = timestamp; }

  if (progress < interval) {
    requestAnimationFrame(check);
  } else {
    if (getResult()) {
      console.log('工作完成!');
    } else {
      console.log(`checking...time passed: ${counter} minute(s).`);
      counter++;
      start = timestamp;
      requestAnimationFrame(check);
    }
  }
}

requestAnimationFrame(check);

點此看完整範例。

NaN(無效的數字)

NaN 表示值為無效的數字(invalid number),會產生 NaN 的原因是

  • 做數學運算時的兩個運算元的資料型別並非都數字或無法轉成有效的十進位或十六進位的數字
  • 無意義的運算,例如:0 / 0Infinity / Infinity 都會得到 NaN

就會產生 NaN。

NaN 有幾個有趣的議題...以下分別討論之。

typeof

NaN 既然表示是無效的數字,依舊還是數字,因此在資料型別的檢測 typeof NaN 結果就是 number,不要被字面上的意思「不是數字」(not a number)給弄糊塗了。

運算結果是 NaN

NaN 與任何數字運算都會得到 NaN。

唯一不大於、不小於、不等於自己的值

NaN 不大於、不小於也不等於任何值,包含 NaN 它自己。

isNaNNumber.isNaN

要如何檢測運算結果是否為有效的數字呢?那麼就來檢測是否為無效的數字-NaN 就可以了。在 ES6 以前,開發者使用 isNaN在數學運算或解析字串後檢測得到的結果是否為合法的數字,其實就是檢測是否為 NaN,其過程為先將輸入值使用 Number 強制轉型為數字,若無法轉為有效的數字而得到 NaN 時就判定等於 NaN,結果得到 true。

範例如下,空物件 {} 經過 isNaN 判斷是 NaN,意即為無效的數字。

isNaN({}) // true,此為無效的數字

// 拆解詳細過程如下...
Number({}) // 先將空物件轉為數字,得到 NaN
isNaN(NaN) // 檢查是否為 NaN,得到 true

其他範例還有...

isNaN(123) // false
isNaN(-1.23) // false
isNaN(5-2) // false
isNaN(0) // false
isNaN('123') // false
isNaN('Hello World') // true
isNaN('2000/01/01') // true
isNaN('') // false
isNaN(true) // false
isNaN(undefined) // true
isNaN('NaN') // true
isNaN(NaN) // true
isNaN(0/0) // true
isNaN(1/0) // false

但這檢測方式的常常會讓開發者得到讓人容易誤解的結果(像是...大多數的人都會爭論...空物件 {} 就真的不等於 NaN 呀),因此 ES6 推出了 Number.isNaNNumber.isNaN 不會經過轉為數字的這個過程,而是直接判斷型別是否為數字且是否等於 NaN。承上範例,使用 Number.isNaN 檢測空物件 {} 是否為 NaN,得到 false。

Number.isNaN({}) // 直接檢查空物件是否為 NaN,得到 false

同樣也來看剛才的範例...

Number.isNaN(123) // false
Number.isNaN(-1.23) // false
Number.isNaN(5-2) // false
Number.isNaN(0) // false
Number.isNaN('123') // false
Number.isNaN('Hello World') // false
Number.isNaN('2000/01/01') // false
Number.isNaN('') // false
Number.isNaN(true) // false
Number.isNaN(undefined) // false
Number.isNaN('NaN') // false
Number.isNaN(NaN) // true
Number.isNaN(0/0) // true
Number.isNaN(1/0) // false

雖然 ES6 出了這個新功能,但不見得所有的瀏覽器都會支援,因此對於較舊的瀏覽器,就掛個 polyfill 來模擬這個新功能。

isNaN(NaN); // true
isNaN(Number.NaN); // true

polyfill 如下。

if (!Number.isNaN) {
  Number.isNaN = function isNaN(x) {
    return x !== x; // NaN 是唯一一個不等於自己的值
  };
}

...

...

面對這種瀏覽器我都很想送它一張恐龍圖,祝它早日被社會淘汰。

恐龍

無限(Infinity)

無限(Infinity)

無限分為正無限(在 ES6 定義為 Number.POSITIVE_INFINITY)和負無限(在 ES6 定義為 Number.NEGATIVE_INFINITY),在數學運算中會得到無限的原因是

  • 除以零,例如:1 / 0 得到 Infinity,-1 / 0 得到 -Infinity
  • 溢位(overflow),例如:Number.MAX_VALUE + Math.pow(2, 970) 得到 Infinity (備註)

又,無限與無限做數學運算,一般來說會得到無限。除了...

  • 無意義的運算,例如:0 / 0Infinity / Infinity 都會得到 NaN
  • 1 / Infinity 得到 0,-1 / Infinity 得到 -0

備註:若運算結果接近 Number.MAX_VALUE 而非 Infinity,則會取一個最接近的值為 Number.MAX_VALUE,這稱為「向下約整」(rounds down);同理,若運算結果接近 Infinity 而非 Number.MAX_VALUE,則會取一個最接近的值為 Infinity,這稱為「向上約整」(rounds up)。

零(Zero)

零分為正零(+0)和負零(-0),正負號在表達方向上是很有用的。其中,產生負零的原因是乘除運算中,運算元的其中一方為負數,例如:-0 / 10 / -1 會得到 -0。

零有幾個有趣的議題...以下分別討論之。

數字轉字串 vs 字串轉數字

不管是正零(+0)或負零(-0),轉字串後一律為「'0'」。

(+0).toString() // "0"
String(+0) // "0"
'' + (+0) // "0"

(-0).toString() // "0"
String(-0) // "0"
'' + (-0) // "0"

相反的,若從字串轉數字,則

  • 字串正零('+0')會轉成數字 0 或報錯,但其實正零一般來說都是表示為「0」。
  • 字串負零('-0')會轉成數字 -0
+'+0' // 0
Number('+0') // 0
JSON.parse('+0') // Uncaught SyntaxError: Unexpected token + in JSON at position 0

+'-0' // -0
Number('-0') // -0
JSON.parse('-0') // -0

如何辨別正零和負零?

正零(+0)或負零(-0)是無法從比較運算子和相等運算子中得到差異。

var a = 0; // 0
var b = 0 / -1; // -0

a == b; // true
-0 == 0; // true

a === b; // true
-0 === 0; // true

0 > -0;	// false
a > b; // false

那到底要如何辨別正零和負零呢?

嗯,啊?

解法的步驟如下

  1. 先將輸入值轉為數字,若為 -0 則 Number(-0) 為 -0,並檢查結果是否等於零
  2. 由於 1 / -0 得到 -Infinity,因此就可檢測輸入值是否為負零
function isNegZero(n) {
  n = Number(n);
  return (n === 0) && (1 / n === -Infinity);
}

// 測試...
isNegZero(-0); // true
isNegZero(0/-1); // true
isNegZero(0); // false

稍後會再提供另一個解法-Object.is(..)

特殊相等性(Special Equality)

針對負零(-0)和 NaN 的比較,除了以上提過的方法外,還可以用 Object.is(..) 來做檢測。

Object.is(..) 會比較兩值是否相等,而 Object.is(..) 的運作和嚴格相等是一樣的,但會將 NaN、-0 和 +0 獨立處理。到底怎麼定義「相等」呢?有興趣的可以參考 MDN 的說明。

var a = Number('Hello World'); // NaN
var b = 0 / -1; // -0

Object.is(a, NaN); // true
Object.is(b, -0); // true
Object.is(b, 0); // false

...

...

世紀難題都被它解決了!讚讚!

Good job!

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...

  • void 運算子可確保運算式不回傳任何值(其實是得到 undefined),並不修改現有值。
  • NaN 表示值為無效的數字,它有以下特色:(1) typeof NaN 為 number;(2) NaN 與任何數字運算都會得到 NaN;(3) NaN 不大於、不小於也不等於任何值,包含 NaN 它自己;(4) 可使用 Number.isNaN 檢測值是否等於 NaN。
  • 無限分為正無限和負無限。
  • 零分為正零(+0)和負零(-0)。
  • 針對負零(-0)和 NaN 的比較,可用 Object.is(..) 來做偵測。

References


同步發表於部落格


上一篇
你懂 JavaScript 嗎?#5 值(Values)Part 1 - 陣列、字串、數字
下一篇
你懂 JavaScript 嗎?#7 原生功能(Natives)
系列文
你懂 JavaScript 嗎?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言