iT邦幫忙

2022 iThome 鐵人賽

DAY 27
1
Modern Web

強化 JavaScript 之 - 程式語感是可以磨練成就的系列 第 27

Day27-JavaScript 的型別轉換 / == 和 === 和 Object.is() 的比較

  • 分享至 

  • xImage
  •  

前言

首先我們要知道 JS 是個弱型別的語言,弱型別和強型別的差異為編譯器或直譯器對型別檢查的寬容程度,弱型別在型別檢查上較為寬鬆,容許隱性型別轉換。強型別在型別檢查上較為嚴格,比較不容許隱性型別轉換。

由於 JS 容許隱性轉換,導致在型別轉換方面有相當多的情況和技巧,所以這篇就來做個整理瞭解各型別的轉換。

以下會介紹幾個型別的轉換,並且分成兩大類,顯性強制轉型 (explicit coercion),也就是有明說要轉成什麼型別,以及隱性強制轉型 (implicit coercion)


數字

顯性強制轉型

顯性轉型常使用 Number 函式,以下示範各型別的轉換結果。

console.log(Number(undefined)) // NaN
console.log(Number(null)) // 0
console.log(Number(true)) // 1
console.log(Number(false)) // 0
console.log(Number('123')) // 123
console.log(Number('Hi')) // NaN
console.log(Number(function () {})); // NaN
console.log(Number({ name: 'Harry' })) // NaN

另外 parseInt() 也是很常用的一個函式,可以將字串轉為數字,不過 parseInt() 會解析到非數字的字元就停止,因此以下範例輸出 7,並且非字串的值無法轉換。

console.log(parseInt("7*6", 10)); // 7
console.log(parseInt(function() {})); // NaN

隱性強制轉型

常用的技巧是用 +-*%/ 等運算符號去進行轉型。

console.log(+'123') // 123
console.log(-'-123') // 123
console.log('123' * 1) // 123

// 運算中,將非 Number 的值轉為 Number
console.log(1 - true) // 0
console.log(1 - null) //  1
console.log(1 * undefined) //  NaN
console.log(2 * ['5']) //  10

// + 號比較特別
console.log(123 + '123') // 123123,number + 字串,先轉字串再相連
console.log(123 + null)  // 123,number + 原始型別,轉 number
console.log(123 + true) // 124,number + 原始型別,轉 number
console.log(123 + {})  // 123[object Object],number + 物件型別,都轉字串後相連

字串

顯性強制轉型

三個轉為字串的函式: String 函式toString()JSON.stringify()

toString() 在後面章節有更詳細的說明,而 JSON.stringify() 能將絕大部分的型別轉成字串,但以下三個例外,都會變成 undefined。

console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(function() {})); // undefined
console.log(JSON.stringify(Symbol())); // undefined

隱性強制轉型

常使用的技巧是在要轉換的值前面加上 '':

const stringOrNum = '' + 2;
console.log(typeof stringOrNum); // string

const arrOrNum = '' + [1, 2];
console.log(typeof arrOrNum); // string
console.log(arrOrNum); // 1,2

布林

顯性強制轉型

使用 Boolean 函式進行轉換

隱性強制轉型

其他型別的值轉成布林值還蠻常發生的,例如在 iffor 迴圈while 迴圈、三元運算子 條件 ? 值1 : 值2、邏輯運算子的 ||&& 等語法的條件判斷部分都會發生隱性轉型,轉換的結果可以看這個網站的表格

! 和 !!

Logical NOT (!),用在布林值上具有反轉的作用,例如將一個為 true 的值轉為 false,反之亦然。

雙驚嘆號(!!)則是"反轉再反轉",經過雙驚嘆號運算後原本的 falsy/truthy value 只會很單純出現 true 和 false 兩種,可以減少某些特別情況時的出錯,在開發上有時會用到。

console.log(!!'Hello World'); // true
console.log(!![]); // true
console.log(!!{}); // true
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!!0); // false
console.log(!!''); // false

=====Object.is() 的比較

這幾種比較也是在 JS 轉型時常討論到的東西,它們的差異在於 == 做值的比較時會自動隱性轉型成比較的值相同型別,而 === 不會。

== 的轉型規則

  1. 若比較的兩個值型別不同,布林值或字串會轉為數字。
  2. 如果其中一方是「物件」型別,而另一方是基本型別的情況下,會調用 valueOf() 或是 toString() 做轉型為原始型別。
const a = '1,2,3';
const b = [1, 2, 3];

console.log(a === b); // false
console.log(a == b); // true,JS 底層調用了 toString() 將 b 陣列轉為字串

=== 的比較原則

不同型別,回傳 false。

  1. 相同原始型別: 值相同回傳 true,值不同回傳 false。
  2. 相同物件型別: 指向相同 reference 時回傳 true,指向不同 reference 回傳 false。

例外: 浮點數 0 為正和負做比較,在解決某些數學問題時,+0 和 -0 是不同的,但在大部分情況下我們不需要考慮這種情境,因此嚴格比較將他們視為相同的。

=== vs Object.is()

兩個都是嚴格比較,但在比較特殊值不一樣:

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

NaN === NaN; // false
-0 === +0; // true

其他型別轉換的函式 - valueOf() & toString()

最後這裡介紹兩個會觸發型別轉換的函式 valueOf()toString()

這兩個函式都可以在物件型別上使用,當物件型別的值碰到需要型轉的時機時,JS 會透過 valueOf()toString() 方法先自動型轉,變成原始型別物件後才比較。開發者也可以自訂 valueOf()toString() 去控制型轉的結果。

valueOf() 通常會返回呼叫它的值本身,我們可以透過覆寫 valueOf() 去返回想要的值。

const a = 3;
const b = '3';
const c = true;
const d = { test: '123', example: 123 };
const e = function () {
  console.log('example');
};
const f = ['test', 'example'];

console.log(a.valueOf()); // 3
console.log(b.valueOf()); // "3"
console.log(c.valueOf()); // true
console.log(d.valueOf()); // { test:'123', example:123 }
console.log(e.valueOf()); // function(){ console.log('example'); }
console.log(f.valueOf()); // ['test', 'example']

toString() 則可以將各型別的值轉為字串。

const a = 3;
const b = '3';
const c = true;
const d = { test: '123', example: 123 };
const e = function () {
  console.log('example');
};
const f = ['test', 'example'];

console.log(a.toString()); // "3"
console.log(b.toString()); // "3"
console.log(c.toString()); // "true"
console.log(d.toString()); // "[object Object]"
console.log(e.toString());
// "function () {
//   window.runnerWindow.proxyConsole.log('example');
// }"
console.log(f.toString()); // "test,example"

雖然兩個函式都可以型轉,但在和不同型別比較時,優先序會有所不同:

  • 強制數字類型轉換和強制原始型別轉換優先會調用 valueOf(),若 valueOf() 返回的不是原始型別值,會再呼叫 toString() 嘗試獲得字串再做進一步處理。
  • 強制字符串轉換會優先調用 toString(),若 toString() 返回的不是字串,會呼叫 valueOf() 嘗試獲取一個原始型別值並轉為字串。

強制字符串轉換的情形可參考 MDN 文件

讀者可以從以下範例更加了解轉換規則:

範例 1.

const obj = {
  valueOf() {
    return 100;
  },
  toString() {
    return "Custom String";
  },
};

console.log(Number(obj)); // 100 (調用 valueOf())
console.log(String(obj)); // "Custom String" (調用 toString())
console.log(obj + 200); // 300 (valueOf() 用於數值計算)
console.log(obj + "!!!"); // "Custom String!!!" (toString() 用於字串連接)

範例 2.

const obj = {
  toString() {
    return '2'
  },

  valueOf() {
    return 1;
  }
};

console.log(obj == '2'); // false,先調用 valueOf() 轉成 1,1 和 '2' 比較,'2' 轉為 2,結果不相等
console.log(obj == 1); // true

補充: 調用 Array.prototype.toString() 時,背後會調用 join() 方法把所有元素轉成字串。

所以以下程式碼片段會印出 false,console.log(arr === true);,為物件型別和原始型別比較,arr 會呼叫 toString() 轉成字串,'0' 和 true 會轉成 0 和 1 比較

var arr = [0];
if (arr) {
    console.log(arr == true);
} else {
    console.log(arr);
}

valueOf() 練習題

此題取自 CodeWars Javascript Magic Function,題目要求寫一個 MagicFunction,它會接收無數個不固定的參數並相加,最後加總的會和一個數字做比對,並回傳 true/false。

MagicFunction(3) == 3; // should return true
MagicFunction(1, 2) == 3; // should return true
MagicFunction(1, 3)(2) == 6; // should return true
MagicFunction(1, 2)(3, 4, 5)(6)(7, 10) == 38; // should return true

思考方向

  1. 因為會接收參數不斷呼叫下去,所以 MagicFunction 需要回傳一個函式,那個函式會進行參數總和的計算,之後再繼續回傳可以做參數加總運算的函式。
  2. MagicFunction 其實可以不用回傳參數總和,它只是要和數字做比對,像題目的範例那樣,所以可以透過 valueOf() 做轉換。

其他細節

  1. result 透過閉包特性儲存。
  2. 透過 filter 過濾不是數字的參數,然後再用 .map(Number) 將加總的值都轉為數字。

解答

function MagicFunction(...args) {
  let result = 0;
  let func = (...args) => {
    result += args.filter((num) => !isNaN(num)).map(Number).reduce((sum, acc) => sum += acc, 0);
    return func;
  }
  func.valueOf = () => result;
  return func(...args);
}

另外在 StackOverflow 上也有個和 valueOf() & toString() 使用相關的討論,有興趣的讀者也可以看一下:
Can (a== 1 && a ==2 && a==3) ever evaluate to true?

Object.prototype.toString()

這個方法能識別各種值的型別:

var number = 1;          // [object Number]
var string = '123';      // [object String]
var boolean = true;      // [object Boolean]
var und = undefined;     // [object Undefined]
var nul = null;          // [object Null]
var obj = {a: 1}         // [object Object]
var array = [1, 2, 3];   // [object Array]
var date = new Date();   // [object Date]
var error = new Error(); // [object Error]
var reg = /a/g;          // [object RegExp]
var func = function a(){}; // [object Function]

function checkType() {
    for (var i = 0; i < arguments.length; i++) {
        console.log(Object.prototype.toString.call(arguments[i]));
    }
}
checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func);

參考文章 & 推薦閱讀

前端工程研究:關於 JavaScript 中物件的 valueOf 方法

Object to primitive conversion

add(2)(3)(4) and add(2,3,4) usage in same function.

你懂 JavaScript 嗎?#8 強制轉型(Coercion)


上一篇
Day26-瞭解 JS 的淺拷貝(Shallow Copy) & 深拷貝(Deep Copy)
下一篇
Day28-寫出更好的 JavaScript 程式碼(上)
系列文
強化 JavaScript 之 - 程式語感是可以磨練成就的30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
json_liang
iT邦研究生 4 級 ‧ 2022-09-27 10:18:12

型別轉換這個真的是一個很重要的知識點!

我要留言

立即登入留言