[譯] [1] + [2] - [3] === 9!? 型別轉換深入研究

達1471183636000發表於2018-04-17

變數值擁有多種格式。而且您可以將一種型別的值轉換為另一種型別的值。這叫型別轉換(也叫顯式轉換)。如果是在後臺中嘗試對不匹配的型別執行操作時發生, 叫 強制轉換(有時也叫隱式轉換)。在這篇文章中,我會引導你瞭解這兩個過程,以便更好地理解過程。讓我們一起深入研究!

型別轉換

原始型別包裝

正如我之前的一篇文章所描述的那樣,幾乎 JavaScript 中的所有原始型別(除了 nullundefined 外)都有圍繞它們原始值的物件包裝。事實上,你可以直接呼叫原始型別的建構函式作為包裝器將一個值的型別轉換為另一個值。

String(123); // '123'
Boolean(123); // true
Number('123'); // 123
Number(true); // 1
複製程式碼

一些原始型別的包裝器,String、Bollean、Number 不會保留很長時間,一旦工作完成,它就消失。(譯者注:JS 中將資料分成兩種型別,原始型別(基本資料型別)和物件型別(引用資料型別)。在物件型別中又有三種特殊型別的引用型別分別是,String、Boolean、Number。這三個就是基本包裝型別。實際上,每當讀取一個基本型別值的時候,後臺就會建立一個對應的基本包裝型別的物件,從而可以呼叫這些型別的方法來運算元據。)

您需要注意,如果您這裡使用了 new 關鍵字,就不再是當前例項。

const bool = new Boolean(false);
bool.propertyName = 'propertyValue';
bool.valueOf(); // false

if (bool) {
  console.log(bool.propertyName); // 'propertyValue'
}
複製程式碼

由於 bool 在這裡是一個新的物件(不是原始值),它的計算結果為 true。

進一步分析

if (1) {
  console.log(true);
}
複製程式碼

效果一樣

if ( Boolean(1) ) {
  console.log(true);
}
複製程式碼

不要畏懼,勇於嘗試。 下面用 Bash 測試。(譯者注:因為沒有找到原始檔,所以我猜測這裡的意思是使用的 if1.js 和 if2.js 是上文的 if 語句檔案,這裡通過 print-code 輸出彙編程式碼。然後通過 awk 列印彙編檔案每句第 4 列字串到檔案裡。最後對比兩個檔案是否一致。藉以推論出上面兩句 if 在程式中的執行是一致的。)

  1. 使用 node.js 將程式碼編譯到程式中
$ node --print-code ./if1.js >> ./if1.asm
複製程式碼
$ node --print-code ./if2.js >> ./if2.asm
複製程式碼
  1. 準備一個指令碼來比較第四列(彙編運算元)- 我故意跳過這裡的記憶體地址,因為它們可能有所不同。
#!/bin/bash

file1=$(awk '{ print $4 }' ./if1.asm)
file2=$(awk '{ print $4 }' ./if2.asm)

[ "$file1" == "$file2" ] && echo "檔案匹配"
複製程式碼
  1. 執行
"檔案匹配"
複製程式碼

parseFloat 函式

這個函式的作用類似於 Number 的建構函式,但對於傳遞的引數來說不那麼嚴格。如果它遇到一個不能成為數字一部分的字元,它將返回一個到該點的值並忽略其餘字元。

Number('123a45'); // NaN
parseFloat('123a45'); // 123
複製程式碼

parseInt 函式

它在解析數字時將數字向下舍入。它可以使用不同的基數。

parseInt('1111', 2); // 15
parseInt('0xF'); // 15

parseFloat('0xF'); // 0
複製程式碼

函式 parseInt 可以猜測基數或讓它作為第二個引數傳遞。有關其中需要考慮的規則列表,請檢視 MDN web docs

如果傳入的數值過大會出問題,所以它不應該被認為是 Math.floor (它也會進行型別轉換)的替代品:

parseInt('1.261e7'); // 1
Number('1.261e7'); // 12610000
Math.floor('1.261e7') // 12610000

Math.floor(true) // 1
複製程式碼

toString 函式

您可以使用 toString 函式將值轉換為字串。這個功能的實現在原型之間有所不同。

如果您覺得您希望更好地理解原型的概念,請隨時檢視我的其他文章: Prototype. The big bro behind ES6 class

String.prototype.toString 函式

返回一個字串的值

const dogName = 'Fluffy';

dogName.toString() // 'Fluffy'
String.prototype.toString.call('Fluffy') // 'Fluffy'

String.prototype.toString.call({}) // Uncaught TypeError: String.prototype.toString requires that 'this' be a String
複製程式碼

Number.prototype.toString 函式

返回轉換為 String 的數字(您可以將 appendix 作為第一個引數傳遞)

(15).toString(); // "15"
(15).toString(2); // "1111"
(-15).toString(2); // "-1111"
複製程式碼

Symbol.prototype.toString 函式

返回 Symbol(${description})

如果你對此感到疑問: 我這裡使用的是 template literals的方式,它可以向你解釋是怎麼輸出這種字串的。

Boolean.prototype.toString 函式

返回 “true” 或 “false”

Object.prototype.toString 函式

Object 呼叫內部 [[Class]] 。它是代表物件型別的標籤。

Object.prototype.toString 返回一個 [object ${tag}] 字串。 要麼它是內建標籤之一 (例如 “Array”, “String”, “Object”, “Date” ), 或者它被明確設定。

const dogName = 'Fluffy';

dogName.toString(); // 'Fluffy' (在這呼叫 String.prototype.toString )
Object.prototype.toString.call(dogName); // '[object String]'
複製程式碼

隨著 ES6 的推出,設定標籤可以使用 Symbols來完成。

const dog = { name: 'Fluffy' }
console.log( dog.toString() ) // '[object Object]'

dog[Symbol.toStringTag] = 'Dog';
console.log( dog.toString() ) // '[object Dog]'
複製程式碼
const Dog = function(name) {
  this.name = name;
}
Dog.prototype[Symbol.toStringTag] = 'Dog';

const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
複製程式碼

你也可以在這裡使用 ES6 類和 getter:

class Dog {
  constructor(name) {
    this.name = name;
  }
  get [Symbol.toStringTag]() {
    return 'Dog';
  }
}

const dog = new Dog('Fluffy');
dog.toString(); // '[object Dog]'
複製程式碼

Array.prototype.toString 函式

在每個元素上呼叫 toString 並返回一個字串,所有的輸出用逗號分隔。

const arr = [
  {},
  2,
  3
]

arr.toString() // "[object Object],2,3"
複製程式碼

隱式轉換

如果您瞭解型別轉換的工作原理,那麼理解隱式轉換會容易得多。

數學運算子

加符號

當在字串與運算元之間使用 + 時結果將返回一個字串。

'2' + 2 // 22
15 + '' // '15'
複製程式碼

你可以用加符號將一個運算元轉換為數字:

+'12' // 12
複製程式碼

其他數學運算子

其他數學運算子,例如 -/ 操作,將自動轉成數字。

new Date('04-02-2018') - '1' // 1522619999999
'12' / '6' // 2
-'1' // -1
複製程式碼

日期, 轉成數字 Unix 時間戳

歎號

如果原始值是 false 的,則使用它將輸出 true,如果 true,則輸出為 false。因此,如果使用兩次,它可以用於將該值轉換為相應的布林值。

!1 // false
!!({}) // true
複製程式碼

ToInt32 按位或

值得一提的是,即使 ToInt32 實際上是一個抽象操作(僅限內部,不可呼叫),它也會把一個值轉換為帶符號 32 位整型

0 | true          // 1
0 | '123'         // 123
0 | '2147483647'  // 2147483647
0 | '2147483648'  // -2147483648 (too big)
0 | '-2147483648' // -2147483648
0 | '-2147483649' // 2147483647 (too small)
0 | Infinity      // 0
複製程式碼

當其中一個運算元為 0 時執行按位或操作將導致不改變另一個運算元的值。

其他隱式轉換

在編碼時,您可能會遇到更多隱式轉換的情況。考慮這個例子

const foo = {};
const bar = {};
const x = {};

x[foo] = 'foo';
x[bar] = 'bar';

console.log(x[foo]); // "bar"
複製程式碼

發生這種情況是因為 foo 和 bar 在轉換為字串時都會轉成 “[object Object]” 。真正發生的是這樣的:

x[bar.toString()] = 'bar';
x["[object Object]"]; // "bar"
複製程式碼

隱式轉換在 template literals也會發生。嘗試在這裡過載 toString 函式:

const Dog = function(name) {
  this.name = name;
}
Dog.prototype.toString = function() {
  return this.name;
}

const dog = new Dog('Fluffy');
console.log(`${dog} is a good dog!`); // "Fluffy is a good dog!"
複製程式碼

隱式轉換也是為什麼比較運算子(==)可能被認為是不好的做法,因為如果它們的型別不匹配,它會嘗試通過強制轉換進行匹配。

檢視這個例子以獲得一個關於比較的有趣事實:

const foo = new String('foo');
const foo2 = new String('foo');

foo === foo2 // false
foo >= foo2 // true
複製程式碼

因為我們在這裡使用了 new 關鍵字,所以 foo 和 foo2 都保留了它們的原始值(這是 'foo' )的包裝。由於他們現在正在引用兩個不同的物件, foo === foo2 結果為 false。關係操作 ( >= ) 在兩邊呼叫 valueOf 函式。因此,在這裡比較原始值記憶體地址, 'foo' >= 'foo' 返回 true

[1] + [2] – [3] === 9

我希望所有這些知識能幫助你揭開本文標題中問題的神祕面紗。讓我們揭開它吧!

  1. [1] + [2] 這些轉換應用 Array.prototype.toString 規則然後連線字串。結果將是 "12"
  • [1,2] + [3,4] 結果是 "1,23,4"
  1. 12 - [3] 將導致 12"3"9
  • 12 - [3,4] 因為 "3,4"不能轉成數字所以得 NaN

總結

儘管很多人可能會建議你避免隱式轉換,但我認為了解它的工作原理非常重要。依靠它可能不是一個好主意,但它對您在除錯程式碼和避免首先出現的錯誤方面大有幫助。

掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄


相關文章