你懂 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 的原因是

就會產生 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),在數學運算中會得到無限的原因是

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

備註:若運算結果接近 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
Number('+0'); // 0
JSON.parse('+0') + '-0'; // Uncaught SyntaxError: Unexpected token + in JSON at position 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!

回顧

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

References


同步發表於2019 鐵人賽


You-Dont-Know-JS javascript 你所不知道的JS 2019鐵人賽 你懂JavaScript嗎? 鐵人賽 You-Dont-Know-JS-Types-and-Grammar NaN 你懂 JavaScript 嗎?2019 iT 邦幫忙鐵人賽 系列文