JavaScript 強制類型轉換

JavaScript 強制類型轉換

做爲 JavaScript 程序員,你必定獲取過當前系統的時間戳。在 ES5 引入 Date.now() 靜態方法以前,下面這段代碼你必定不會陌生:javascript

var timestamp = +new Date(); // timestamp 就是當前的系統時間戳,單位是 ms

你確定據說過 JavaScript 的強制類型轉換,你能指出這段代碼裏哪裏用到了強制類型轉換嗎?java

幾乎全部 JavaScript 程序員都接觸過強制類型轉換 —— 不管是有意的仍是無心的。強制類型轉換致使了不少隱蔽的 BUG,可是強制類型轉換同時也是一種很是有用的技術,咱們不該該因噎廢食。程序員

在本文中咱們來詳細探討一下 JavaScript 的強制類型轉換,以便咱們能夠在避免踩坑的狀況下最大化利用強制類型轉換的便捷。數組

類型轉換和強制類型轉換

類型轉換髮生在靜態類型語言的編譯階段,而強制類型轉換髮生在動態類型語言的運行時(runtime),所以在 JavaScript 中只有強制類型轉換。瀏覽器

強制類型轉換通常還可分爲 隱式強制類型轉換(implicit coercion和 _顯式強制類型轉換(explicit coercion)_。函數

從代碼中能夠看出轉換操做是隱式的仍是顯式的,顯式強制類型轉換很容易就能看出來,而隱式強制類型轉換可能就沒有這麼明顯了。工具

好比:this

var a = 21;

var b = a + '';

var c = String(a);

對於變量 b 而言,這次強制類型轉換是隱式的。+ 操做符在其中一個操做數是字符串時進行的是字符串拼接操做,所以數字 21 會被轉換爲相應的字符串 "21"prototype

然而 String(21) 則是很是典型的顯式強制類型轉換。日誌

這兩種強制轉換類型的操做都是將數字轉換爲字符串。

不過「顯式」仍是「隱式」都是相對而言的。好比若是你知道 a + "" 是怎麼回事,那麼對你來講這可能就是「顯式」的。反之,若是你不知道 String(a) 能夠用來字符串強制類型轉換,那麼它對你來講可能就是「隱式」的。

抽象值操做

在介紹強制類型轉換以前,咱們須要先了解一下字符串、數字和布爾值之間類型轉換的基本規則。在 ES5 規範中定義了一些「抽象操做」和轉換規則,在這咱們介紹一下 ToPrimitiveToStringToNumberToBoolean。注意,這些操做僅供引擎內部使用,和平時 JavaScript 代碼中的 .toString() 等操做不同。

ToPrimitive

你能夠將 ToPrimitive 操做看做是一個函數,它接受一個 input 參數和一個可選的 PreferredType 參數。ToPrimitive 抽象操做會將 input 參數轉換成一個原始值。若是一個對象能夠轉換成不止一種原始值,可使用 PreferredType 指定抽象操做的返回類型。

根據不一樣的輸入類型,ToPrimitive 的轉換操做以下:

輸入類型 操做 / 返回值
Undefined 自身(無轉換操做)
Null 自身(無轉換操做)
Boolean 自身(無轉換操做)
Number 自身(無轉換操做)
String 自身(無轉換操做)
Object 返回 Objectdefault valueObjectdefault value 經過在該對象上傳遞 PreferredType 參數給內部操做 [[DefaultValue]](hint) 得到。[[DefaultValue]](hint) 的實現請往下看。

[[DefaultValue]](hint) 內部操做

在對象 O 上調用內部操做 [[DefaultValue]] 時,根據 hint 的不一樣,其執行的操做也不一樣,簡化版(具體可參考 ES5 規範 8.12.8 節)以下:

  • 若是 hintString

    • 若是 OtoString 屬性是函數;

      • O 設置爲 this 值並調用 toString 方法,將返回值賦值給 val
      • 若是 val 是原始值類型則返回;
    • 若是 OvalueOf 屬性是函數;

      • O 設置爲 this 值並調用 valueOf 方法,將返回值賦值給 val
      • 若是 val 是原始值類型則返回;
    • 拋出 TypeError 錯誤。
  • 若是 hintNumber

    • 若是 OvalueOf 屬性是函數;

      • O 設置爲 this 值並調用 valueOf 方法,將返回值賦值給 val
      • 若是 val 是原始值類型則返回;
    • 若是 OtoString 屬性是函數;

      • O 設置爲 this 值並調用 toString 方法,將返回值賦值給 val
      • 若是 val 是原始值類型則返回;
    • 拋出 TypeError 錯誤。
  • 若是 hint 參數爲空;

    • 若是 ODate 對象,則和 hintString 時一致;
    • 不然和 hintNumber 時一致。

ToString

原始值的字符串化的規則以下:

  • null 轉化爲 "null"
  • undefined 轉化爲 "undefined"
  • true 轉化爲 "true"
  • false 轉化爲 "false";
  • 數字的字符串化遵循通用規則,如 21 轉化爲 "21",極大或者極小的數字使用指數形式,如:

    var num = 3.912 * Math.pow(10, 50);
    
    num.toString(); // "3.912e50"
  • 對於普通對象,若是對象有自定義的 toString() 方法,字符串化時就會調用該自定義方法並使用其返回值,不然返回的是內部屬性 [[Class]] 的值,好比 "object [Object]"。須要注意的是,數組默認的 toString() 方法通過了從新定義,其會將全部元素字符串化以後再用 "," 鏈接起來,如:

    var arr = [1, 2, 3];
    
    arr.toString(); // "1,2,3"

ToNumber

在 ES5 規範中定義的 ToNumber 操做能夠將非數字值轉換爲數字。其規則以下:

  • true 轉換爲 1
  • false 轉換爲 0
  • undefined 轉換爲 NaN
  • null 轉換爲 0
  • 針對字符串的轉換基本遵循數字常量的相關規則。處理失敗則返回 NaN
  • 對象會先被轉換爲原始值,若是返回的是非數字的原始值,則再遵循上述規則將其強制轉換爲數字。

在將某個值轉換爲原始值的時候,會首先執行抽象操做 ToPrimitive,若是結果是數字則直接返回,若是是字符串再根據相應規則轉換爲數字。

參照上述規則,如今咱們能夠一步一步來解釋本文開頭的那行代碼了。

var timestamp = +new Date(); // timestamp 就是當前的系統時間戳,單位是 ms

其執行步驟以下:

  • new 操做符比 + 操做符優先級更高,所以先執行 new Date() 操做,生成一個新的 Date 實例;
  • 一元操做符 + 在其操做數爲非數字時,會對其進行隱式強制類型轉換數字

    • hintNumber

      • Date 實例的 valueOf 屬性指向的是 Date.prototype.valueOf,是一個函數;
      • this 指向 Date 實例並調用 valueOf 函數,得到返回值;
      • 返回值是一個數字,直接將其返回;
  • 隱式強制類型轉換得到的值賦值給 timestamp 變量。

有了以上知識,咱們就能夠實現一些比較好玩的東西了,好比將數字和對象相加:

var a = {
  valueOf: function() {
    return 18;
  }
};

var b = 20;

+a; // 18
Number(a); // 18
a + b; // 38
a - b; // -2

順帶提一下,從 ES5 開始,使用 Object.create(null) 建立的對象,其 [[Prototype]] 屬性爲 null 所以沒有 valueOf()toString() 方法,所以沒法進行強制類型轉換。請看以下示例:

var a = {};
var b = Object.create(null);

+a; // NaN
+b; // Uncaught TypeError: Cannot convert object to primitive value
a + ''; // "[object Object]"
b + ''; // Uncaught TypeError: Cannot convert object to primitive value

ToBoolean

JavaScript 中有兩個關鍵字 truefalse,分別表示布爾類型的真和假。咱們常常會在 if 語句中將 0 做爲假值條件,1 做爲真值條件,這也利用了強制類型轉換。咱們能夠將 true 強制類型轉換爲 1false 強制類型轉換爲 0,反之亦然。然而 true1 並非一回事,false0 也同樣。

假值

在 JavaScript 中值能夠分爲兩類:

  • 能夠被強制類型轉換爲 false 的值
  • 其餘(被強制類型轉換爲 true 的值)

在 ES5 規範中下列值被定義爲假值:

  • undefined
  • null
  • false
  • +0-0NaN
  • ""

假值的布爾強制類型轉換結果爲 false

在假值列表之外的值都是真值。

⚠️ 例外

規則不免有例外。剛說了除了假值列表之外的全部其餘值都是真值,然而你能夠在現代瀏覽器的控制檯中執行下面幾行代碼試試:

Boolean(document.all);
typeof document.all;

獲得的結果應該是 false"undefined"。然而若是你直接執行 document.all 獲得的是一個類數組對象,包含了頁面中全部的元素。document.all 實際上不能算是 JavaScript 語言的範疇,這是瀏覽器在特定條件下建立一些外來(exotic)值,這些就是「假值對象」。

假值對象看起來和普通對象並沒有二致(都有屬性,document.all 甚至能夠展爲數組),可是其強制類型轉換的結果倒是 false

在 ES5 規範中,document.all 是惟一一個例外,其緣由主要是爲了兼容性。由於老代碼可能會這麼判斷是不是 IE:

if (document.all) {
  // Internet Explorer
}

在老版本的 IE 中,document.all 是一個對象,其強制類型轉換結果爲 true,而在現代瀏覽器中,其強制轉換結果爲 false

真值

除了假值之外都是真值。

好比:

var a = 'false';
var b = '0';
var c = "''";

var d = Boolean(a && b && c);

d; // ?

dtrue 仍是 false 呢?

答案是 true。這些值都是真值,相信不須要過多分析。

一樣,如下幾個值同樣都是真值:

var a = [];
var b = {};
var c = function() {};

顯式強制類型轉換

顯式強制類型轉換很是常見,也不會有什麼坑,JavaScript 中的顯式類型轉換和靜態語言中的很類似。

字符串和數字之間的顯式轉換

字符串和數字之間的相互轉換靠 String()Number() 這兩個內建函數實現。注意在調用時沒有 new 關鍵字,只是普通函數調用,不會建立一個新的封建對象。

var a = 21;
var b = '2.71828';

var c = String(a);
var d = Number(b);

c; // "21"
d; // 2.71828

除了直接調用 String() 或者 Number() 方法以外,還能夠經過別的方式顯式地進行數字和字符串之間的相互轉換:

var a = 21;
var b = '2.71828';

var c = a.toString();
var d = +b;

c; // "21"
d; // 2.71828

雖然 a.toString() 看起來很像顯式的,然而其中涉及了隱式轉換,由於 21 這樣的原始值是沒有方法的,JavaScript 自動建立了一個封裝對象,並調用了其 toString() 方法。

+b 中的 + 是一元運算符,+ 運算符會將其操做數轉換爲數字。而 +b 是顯式仍是隱式就取決於開發者自身了,本文以前也提到過,顯式仍是隱式都是相對的。

顯式轉換爲布爾值

和字符串與數字之間的相互轉換同樣,Boolean() 能夠將參數顯示強制轉換爲布爾值:

var a = '';
var b = 0;
var c = null;
var d = undefined;

var e = '0';
var f = [];
var g = {};

Boolean(a); // false
Boolean(b); // false
Boolean(c); // false
Boolean(d); // false

Boolean(e); // true
Boolean(f); // true
Boolean(g); // true

不過咱們不多會在代碼中直接用 Boolean() 函數,更常見的是用 !! 來強制轉換爲布爾值,由於第一個 ! 會將操做數強制轉換爲布爾值,並反轉(真值反轉爲假值,假值反轉爲真值),而第二個 ! 會將結果反轉回原值:

var a = '';
var b = 0;
var c = null;
var d = undefined;

var e = '0';
var f = [];
var g = {};

!!a; // false
!!b; // false
!!c; // false
!!d; // false

!!e; // true
!!f; // true
!!g; // true

不過更常見的狀況是相似 if(...) {} 這樣的代碼,在這個上下文中,若是咱們沒有使用 Boolean() 或者 !! 轉換,就會自動隱式地進行 ToBoolean 轉換。

三元運算符也是一個很常見的布爾隱式強制類型轉換的例子:

var a = 21;
var b = 'hello';
var c = false;

var d = a ? b : c;

d; // "hello"

在執行三元運算的時候,先對 a 進行布爾強制類型轉換,而後根據結果返回 : 先後的值。

隱式強制類型轉換

大部分被詬病的強制類型轉換都是隱式強制類型轉換。可是隱式強制類型轉換真的一無可取嗎?並不必定,引擎在必定程度上簡化了強制類型轉換的步驟,這對於有些狀況來講並非好事,而對於另外一些狀況來講可能並不必定是壞事。

字符串和數字之間的隱式強制類型轉換

在上一節咱們已經介紹了字符串和數字之間的顯式強制類型轉換,在這一節咱們來講說他們二者之間的隱式強制類型轉換。

+ 運算符既能夠用做數字之間的相加也能夠經過重載用於字符串拼接。咱們可能以爲若是 + 運算符兩邊的操做數有一個或以上是字符串就會進行字符串拼接。這種想法並不徹底錯誤,但也不是徹底正確的。好比如下代碼能夠驗證這句話是正確的:

var a = 21;
var b = 4;

var c = '21';
var d = '4';

a + b; // 25
c + d; // "214"

可是若是 + 運算符兩邊的操做數不是字符串呢?

var arr0 = [1, 2];
var arr1 = [3, 4];

arr0 + arr1; // ???

上面這條命令的執行結果是 "1,23,4"ab 都不是字符串,爲何 JavaScript 會把 ab 都轉換爲字符串再進行拼接?

根據 ES5 規範 11.6.1 節,若是 + 兩邊的操做數中,有一個操做數是字符串或者能夠經過如下步驟轉換爲字符串,+ 運算符將進行字符串拼接操做:

  • 若是一個操做數爲對象,則對其調用 ToPrimitive 抽象操做;
  • ToPrimitive 抽象操做會調用 [[DefaultValue]](hint),其中 hintNumber

這個操做和上面所述的 ToNumber 操做一致,再也不重複。

在這個操做中,JavaScript 引擎對其進行 ToPrimitive 抽象操做的時候,先執行 valueOf() 方法,可是因爲其 valueOf() 方法返回的是數組,沒法獲得原始值,轉而調用 toString() 方法,toString() 方法返回了以 , 拼接的全部元素的字符串,即 1,23,4+ 運算符再進行字符串拼接,獲得結果 1,23,4

簡單來講,只要 + 的操做數中有一個是字符串,或者能夠經過上述步驟獲得字符串,就進行字符串拼接操做;其他狀況執行數字加法。

因此如下這段代碼可謂隨處可見:

var a = 21;

a + ''; // "21"

利用隱式強制類型轉換將非字符串轉換爲字符串,這樣轉換很是方便。不過經過 a + "" 和直接調用 String(a) 之間並非徹底同樣,有些細微的差異須要注意一下。a + "" 會對 a 調用 valueOf() 方法,而後再經過上述的 ToString 抽象操做轉換爲字符串。而 String(a) 則會直接調用 toString()

雖然返回值都是字符串,然而若是 a 是對象的話,結果可能出乎意料!

好比:

var a = {
  valueOf: function() {
    return '21';
  },
  toString: function() {
    return '6';
  }
};

a + ''; // "42"
String(a); // "6"

不過大部分狀況下也不會寫這麼奇怪的代碼,若是你真的要擴展 valueOf() 或者 toString() 方法的話,請留意一下,由於你可能無心間影響了強制類型轉換的結果。

那麼從字符串轉換爲數字呢?請看下面的例子:

var a = '2.718';
var b = a - 0;

b; // 2.718

因爲 - 操做符不像 + 操做符有重載,- 只能進行數字減法操做,所以若是操做數不是數字的話會被強制轉換爲數字。固然,a * 1a / 1 也能夠,由於這兩個運算符也只能用於數字。

- 用於對象會怎麼樣呢?好比:

var a = [3];
var b = [1];

a - b; // 2

- 只能執行數字減法,所以會對操做數進行強制類型轉換爲數字,根據前面所述的步驟,數組會調用其 toString() 方法得到字符串,而後再轉換爲數字。

布爾值到數字的隱式強制類型轉換

假設如今你要實現這麼一個函數,在它的三個參數中,若是有且只有一個參數爲真值則返回 true,不然返回 false,你該怎麼寫?

簡單一點的寫法:

function onlyOne(x, y, z) {
  return !!((x && !y && !z) || (!x && y && !z) || (!x && !y && z));
}

onlyOne(true, false, false); // true
onlyOne(true, true, false); // false
onlyOne(false, false, true); // true

三個參數的時候代碼好像也不是很複雜,那若是是 20 個呢?這麼寫確定過於繁瑣了。咱們能夠用強制類型轉換來簡化代碼:

function onlyOne(...args) {
  return (
    args.reduce(
      (accumulator, currentValue) => accumulator + !!currentValue,
      0
    ) === 1
  );
}

onlyOne(true, false, false, false); // true
onlyOne(true, true, false, false); // false
onlyOne(false, false, false, true); // true

在上面這個改良版的函數中,咱們使用了數組的 reduce() 方法來計算全部參數中真值的數量,先使用隱式強制類型轉換把參數轉換成 true 或者 false,再經過 + 運算符將 true 或者 false 隱式強制類型轉換成 1 或者 0,最後的結果就是參數中真值的個數。

經過這種改良版的代碼,咱們能夠很簡單的寫出 onlyTwo()onlyThree() 的函數,只須要改一個數字就行了。這無疑是一個很大的提高。

隱式強制類型轉換爲布爾值

在如下狀況中會發生隱式強制類型轉換:

  • if (...) 語句中的條件判斷表達式;
  • for (..; ..; ..) 語句中的條件判斷表達式,也就是第二個;
  • while (..)do..while(..) 循環中的條件判斷表達式;
  • .. ? .. : .. 三元表達式中的條件判斷表達式,也就是第一個;
  • 邏輯或 || 和邏輯與 && 左邊的操做數,做爲條件判斷表達式。

在這些狀況下,非布爾值會經過上述的 ToBoolean 抽象操做被隱式強制類型轉換爲布爾值。

||&&

JavaScript 中的邏輯或和邏輯與運算符和其餘語言中的不太同樣。在別的語言中,其返回值類型是布爾值,然而在 JavaScript 中返回值是兩個操做數之一。所以在 JavaScript 中,||&& 被稱做選擇器運算符可能更合適。

根據 ES5 規範 11.11 節:

||&& 運算符的返回值不必定是布爾值,而是兩個操做數中的其中一個。

好比:

var a = 21;
var b = 'xyz';
var c = null;

a || b; // 21
a && b; // "xyz"

c || b; // "xyz"
c && b; // null

若是 || 或者 && 左邊的操做數不是布爾值類型的話,則會對左邊的操做數進行 ToBoolean 操做,根據結果返回運算符左邊或者右邊的操做數。

對於 || 來講,左邊操做數的強制類型轉換結果若是爲 true 則返回運算符左邊的操做數,若是是 false 則返回運算符右邊的操做數。

對於 && 來講則恰好相反,左邊的操做數強制類型轉換結果若是爲 true 則返回運算符右邊的操做數,若是是 false 則返回運算符左邊的操做數。

||&& 返回的是兩個操做數之一,而非布爾值。

在 ES6 的函數默認參數出現以前,咱們常常會看到這樣的代碼:

function foo(x, y) {
  x = x || 'x';
  y = y || 'y';

  console.log(x + ' ' + y);
}

foo(); // "x y"
foo('hello'); // "hello y"

看起來和咱們預想的一致。可是,若是是這樣調用呢?

foo('hello world', ''); // ???

上面的執行結果是 hello world y,爲何?

在執行到 y = y || "y" 的時候,JavaScript 對運算符左邊的操做數進行了布爾隱式強制類型轉換,其結果爲 false,所以運算結果爲運算符右邊的操做數,即 "y",所以最後打印出來到日誌是 "hello world y" 而非咱們預想的 hello world

因此這種方式須要確保傳入的參數不能有假值,不然就可能和咱們預想的不一致。若是參數中可能存在假值,則應該有更加明確的判斷。

若是你看過壓縮工具處理後的代碼的話,你可能常常會看到這樣的代碼:

function foo() {
  // 一些代碼
}

var a = 21;

a && foo(); // a 爲假值時不會執行 foo()

這時候 && 就被稱爲守護運算符(guard operator),即 && 左邊的條件判斷表達式結果若是不是 true 則會自動終止,不會判斷操做符右邊的表達式。

因此在 if 或者 for 語句中咱們使用 ||&& 的時候,if 或者 for 語句會先對 ||&& 操做符返回的值進行布爾隱式強制類型轉換,再根據轉換結果來判斷。

好比:

var a = 21;
var b = null;
var c = 'hello';

if (a && (b || c)) {
  console.log('hi');
}

在這段代碼中,a && (b || c) 的結果實際是 'hello' 而非 true,而後 if 再經過隱式類型轉換爲 true 才執行 console.log('hi')

Symbol 的強制類型轉換

ES6 中引入了新的基本數據類型 —— Symbol。然而它的強制類型轉換有些不同,它支持顯式強制類型轉換,可是不支持隱式強制類型轉換。

好比:

var s = Symbol('hi');

String(s); // 'Symbol(hi)'
s + ''; // Uncaught TypeError: Cannot convert a Symbol value to a string

並且 Symbol 不能強制轉換爲數字,好比:

var s = Symbol('hi');

s - 0; // Uncaught TypeError: Cannot convert a Symbol value to a number

Symbol 的布爾強制類型轉換都是 true

相關文章
相關標籤/搜索