揭祕 JavaScript 弱類型

前言

關於強制類型轉換是一個設計上的缺陷仍是有用的特性,這一爭論從JavaScript 誕生之日起就開始了。在不少的 JavaScript 書籍中強制類型轉換被說成是危險、晦澀和糟糕的設計。數組

然而在不少實際應用中都運用了大量的弱類型轉換:app

alert({}); // 彈框提示:[object Object]

"0" + 1; // "01"
複製代碼

因爲這一特性,誕生了一系列有意思的題:函數

[] + {}; // "[object Object]"
{} + []; // 0
複製代碼
if (a == 1 && a == 2) {
  alert('hello world!'); // 他是可能的
}
複製代碼

一、類型

JavaScript的類型分爲原始類型與引用類型。ui

  • 原始類型
    • 空值( null )
    • 未定義( undefined )
    • 布爾值( boolean )
    • 數字( number )
    • 字符串( string )
    • 符號( symbol ,ES6 中新增)
  • 引用類型
    • 對象( object )
      • 數組 (array)
      • 方法 (function)

咱們能夠用 typeof 運算符來查看值的類型,它返回的是類型的字符串值。this

typeof undefined === "undefined"; // true
typeof true === "boolean"; // true
typeof 42 === "number"; // true
typeof "42" === "string"; // true
typeof { life: 42 } === "object"; // true
// ES6中新加入的類型
typeof Symbol() === "symbol"; // true
複製代碼

你可能注意到 null 類型不在此列。它比較特殊, typeof 對它的處理有問題:編碼

typeof null === "object"; // true
複製代碼

這個 bug 由來已久,在 JavaScript 中已經存在了將近二十年,也許永遠也不會修復,由於這牽涉到太多的 Web 系統,「修復」它會產生更多的bug,令許多系統沒法正常工做。spa

再來看看數組。JavaScript 支持數組,那麼它是否也是一個特殊類型?prototype

typeof [1,2,3] === "object"; // true
複製代碼

不,數組也是對象。確切地說,它也是 object 的一個「子類型」設計

最後看一組code

typeof false; // "boolean"
typeof new Boolean(false); // "object"

typeof 1; // "number"
typeof new Number(1); // "object"

複製代碼

二、類型轉換

將值從一種類型轉換爲另外一種類型一般稱爲類型轉換。 類型轉換又分爲隱式顯式,如:

var a = 42;
var b = a + ""; // 隱式強制類型轉換
var c = String( a ); // 顯式強制類型轉換
複製代碼

2.一、類型抽象操做

ES5 規範第 9 節中定義了一些「抽象操做」(即「僅供內部使用的操做」)和轉換規則。即:

  • ToString - 將非字符串轉爲字符串。
  • ToNumber - 將非數字轉爲數字。
  • ToBoolean - 將非布爾值轉爲布爾值。
  • ToPrimitive - 將引用類型值轉爲原始類型值。

這些操做都是由js內部實現,並不是實際的函數。接下來咱們進行解析其內部實現。

將 引用類型值 強制類型轉換爲 string、number、boolean 是經過 ToPrimitive 抽象操做來完成的,咱們在此略過,稍後會詳細介紹。

2.1.一、ToString

抽象操做 ToString ,它負責處理非字符串到字符串的強制類型轉換。

基本類型值的字符串化規則爲: null 轉換爲 "null" , undefined 轉換爲 "undefined" , true 轉換爲 "true" 。數值遵循常規規則。

2.1.二、ToNumber

有時咱們須要將非數字值看成數字來使用,好比數學運算。爲此 ES5 規範在 9.3 節定義了抽象操做 ToNumber 。

其中 true 轉換爲 1 , false 轉換爲 0 。 undefined 轉換爲 NaN , null 轉換爲 0 。

ToNumber操做字符串時,會判斷該字符是否符合數字值格式。處理失敗時返回 NaN。其處理規則與Number函數類似:

Number( "" ); // 0
  Number( "a1" ); // NaN
複製代碼

2.1.三、 ToBoolean

JavaScript 中有兩個關鍵詞 true 和 false ,分別表明布爾類型中的真和假。咱們常誤覺得數值 1 和 0 分別等同於 true 和 false 。在有些語言中多是這樣,但在 JavaScript 中布爾值和數字是不同的。

假值
  • undefined
  • null
  • ""
  • false
  • +0、-0 和 NaN

假值的布爾強制轉換的結果均爲 false 。

真值

假值列表之外的值都是真值。即布爾強制轉換後均爲true。 因此空數組[]、空對象{}、字符串"null"、字符串"undefined" 等都爲真值。

2.1.四、ToPrimitive

抽象操做 ToPrimitive ,它負責將引用類型值轉化爲基本類型值。其具體規則爲: 一、檢查該值是否有 valueOf() 方法 二、若是有而且返回基本類型值,就使用該值進行強制類型轉換 三、若是沒有就使用 toString() 的返回值(若是存在)來進行強制類型轉換。 四、若是 valueOf() 和 toString() 均不返回基本類型值,會產生 TypeError 錯誤。

var a = {
  valueOf: function(){
    return "42";
  }
};
Number( a ); // a.valueOf() -> "42" -> 42
String( a ); // a.toString() -> "[object Object]"

var b = {
  toString: function(){
    return "42";
  }
};
Number( b ); // b.valueOf() -> {}; b.toString() -> "42" -> 42
String( b ); // b.toString() -> "42"

var c = [4,2];
c.toString = function(){
  return this.join( "" ); // "42"
};
Number( c ); // c.toString() -> "42" -> 42

Number( [4, 2] ); // [4,2].toString() -> "4,2" -> NaN
複製代碼

a + "" (隱式)和前面的 String(a) (顯式)之間有一個細微的差異須要注意:

var a = {
  valueOf: function() { return 42; },
  toString: function() { return 4; }
};
a + ""; // a.valueOf() -> 42 -> "42"
String( a ); // a.toString() -> 4 -> "4"
複製代碼

2.二、顯式類型轉換

函數:

  • Number 用於轉換爲數值
  • String 用於轉換爲字符串
  • Boolean 用於轉換爲布爾值
  • Object.prototype.toString 用於將對象轉換爲字符串
  • Array.prototype.toString 用於將數組轉換爲字符串
  • Number.prototype.toString 用於將數值轉換爲字符串
  • ...

操做符:

  • !x 用於將x轉爲非布爾值,非。
  • (+x) 用於將x轉爲數值。

2.三、隱式類型轉換

隱式強制類型轉換指的是那些隱蔽的強制類型轉換,反作用也不是很明顯。換句話說,你本身以爲不夠明顯的強制類型轉換均可以算做隱式強制類型轉換。

主要分爲:

  • 部分類型值之間的加減乘除等(算式)
  • 部分類型值之間比較大小
  • 抽象相等 ==
  • 條件語句 (其餘類型值轉布爾值)

這裏主要講算式與弱相等。

2.3.一、算式

原始類型

數字 + 字符串 number + string = ToString(number) + string

1 + '2'; // 
複製代碼

數字 + 布爾 number + boolean = number + ToNumber(boolean)

1 + true; // 2 + ToNumber()
複製代碼

字符串 + 布爾 string + boolean = string + ToString( boolean )

'1' + true; // "1true"
複製代碼

原始類型 -x÷ 原始類型 primitive -x÷ primitive = ToNumber(primitive) -x÷ ToNumber(primitive)

引用類型

引用類型在參與計算時,會先通過ToPrimitive轉換成原始類型,而後按照上述規則參與計算。

1 + {}; // "1[object Object]"
({toString(){return 1}}) + ({toString(){return 1}}); // 2
({toString(){return 1}}) + ({toString(){return '1'}}); // "11"
複製代碼

2.3.二、抽象相等

抽象相等也稱爲寬鬆相等,它的規則正是隱式強制類型轉換被詬病的緣由,很容易致使 bug,實際上他的規則很是簡單。

首先,有幾個很是規的狀況須要注意。

  • NaN 不等於 NaN
  • +0 等於 -0
定義

== 在比較兩個不一樣類型的值時會發生隱式強制類型轉換,會將其中之一或二者都轉換爲相同的類型後再進行比較。

一、字符串和數字之間的相等比較

這裏用字符串和數字的例子來解釋 == 中的強制類型轉換:

var a = 42;
var b = "42";
a === b; // false
a == b; // true
複製代碼

由於沒有強制類型轉換,因此 a === b 爲 false , 42 和 "42" 不相等。

而 a == b 是寬鬆相等,即若是兩個值的類型不一樣,則對其中之一或二者都進行強制類型轉換。

具體怎麼轉換?是 a 從 42 轉換爲字符串,仍是 b 從 "42" 轉換爲數字?

ES5 規範 11.9.3.4-5 這樣定義: (1) 若是 Type(x) 是數字, Type(y) 是字符串,則返回 x == ToNumber(y) 的結果。 (2) 若是 Type(x) 是字符串, Type(y) 是數字,則返回 ToNumber(x) == y 的結果。

其中ToNumber 爲抽象操做的規則前面已經介紹過。

二、其餘類型和布爾類型之間的相等比較

== 最容易出錯的一個地方是 true 和 false 與其餘類型之間的相等比較。 例如:

var a = "42";
var b = true;
a == b; // false
複製代碼

咱們都知道 "42" 是一個真值(見本章前面部分),爲何 == 的結果不是 true 呢?緣由既簡單又複雜,讓人很容易掉坑裏,不少 JavaScript 開發人員對這個地方並未引發足夠的重視。

規範 11.9.3.6-7 是這樣說的: (1) 若是 Type(x) 是布爾類型,則返回 ToNumber(x) == y 的結果 (2) 若是 Type(y) 是布爾類型,則返回 x == ToNumber(y) 的結果。

根據規則 "42" == true ,通過ToNumber(true) -> 1; "42" == 1,再通過ToNumber("42") -> 42; 42 == 1,因此輸出false。

三、null 和 undefined 之間的相等比較

null 和 undefined 之間的 == 也涉及隱式強制類型轉換。

ES5 規範 11.9.3.2-3 規定: (1) 若是 x 爲 null , y 爲 undefined ,則結果爲 true 。 (2) 若是 x 爲 undefined , y 爲 null ,則結果爲 true 。

在 == 中 null 和 undefined 相等(它們也與其自身相等),除此以外其餘值都不存在這種狀況。 也就是說除null和undefined外的全部類型值都不與他們抽象相等。

var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false
複製代碼
四、對象和非對象之間的相等比較

關於引用類型(對象 / 函數 / 數組)和原始類型值(字符串 / 數字 / 布爾值)之間的相等比較。

ES5 規範 11.9.3.8-9 作以下規定: (1) 若是 Type(x) 是字符串或數字, Type(y) 是對象,則返回 x == ToPrimitive(y) 的結果; (2) 若是 Type(x) 是對象, Type(y) 是字符串或數字,則返回 ToPromitive(x) == y 的結果。

例如

var a = 42;
var b = [ 42 ];
a == b; // true
複製代碼

[ 42 ] == 42, 通過ToPrimitive([ 42 ]) -> [ 42 ].toString() -> "42"; "42" == 42, 再通過ToNumber("42") -> 42; 42 == 42; 返回true

五、其餘少見的狀況
Number.prototype.valueOf = function() {
  return 3;
};
new Number( 2 ) == 3; // true
複製代碼

而 2 == 3 不會有這種問題,由於 2 和 3 都是數字基本類型值,不會調用 Number.prototype.valueOf() 方法。而 Number(2) 涉及 ToPrimitive 強制類型 轉換,所以會調用 valueOf() 。

還有文章開頭的題:

if (a == 1 && a == 2) {
  alert('hello world!');
}
複製代碼

你也許以爲這不可能,由於 a 不會同時等於 1 和 2 。但「同時」一詞並不許確,由於 a == 1 在 a == 2 以前執行。 若是讓 a.valueOf() 每次調用都產生反作用,好比第一次返回 1 ,第二次返回 2 ,就會出現這樣的狀況。

var i = 1;
Number.prototype.valueOf = function() {
  return i++;
};
var a = new Number( 42 );
if (a == 1 && a == 2) {
  console.log( "Yep, this happened." );
  alert('hello world!');
}
複製代碼

再次強調,千萬不要這樣,也不要所以而抱怨強制類型轉換。對一種機制的濫用並不能成爲詬病它的藉口。咱們應該正確合理地運用強制類型轉換,避免這些極端的狀況。

總結

本文介紹了 JavaScript 的數據類型之間的轉換:包括顯式和隱式。

在處理強制類型轉換的時候要十分當心,尤爲是隱式強制類型轉換。在編碼的時候,要知其然,還要知其因此然,並努力讓代碼清晰易讀。

本文大部分例子與描述來自 《你不知道的JavaScript》。

相關文章
相關標籤/搜索