2016-08-03

JS:如何判斷真假值?

如何判斷真假值?

結論:

單值判斷:
  • false0, 空字串("", 字串長度為 0), NaNnull 以及 undefined 是為 false
  • 其他值為 true
等於(==)判斷:
  • 兩 object 間的比較只是比較其內部參照是否相同
  • 若 object 與基本資料型別 numberbooleanstring 比較, object 會先呼叫 object.valueOf(), 若是傳回 object, 則會再呼叫 object.toString() 做為比較值, 再比較
  • 可轉型的基本資料型別 numberbooleanstring 彼此比較, 如果型別不同, 則都轉成 number 後, 再比較
  • 與不轉型的基本資料型別 nullundefined 比較, 除與 nullundefined 比較結果為 true 外, 其他都是 false
  • NaN 不等於 NaN

廢話:

js 是 我學過最 容易被誤解的語言.

正文:

js 的基本資料型別是
  • number 不管整數, 浮點數, 反正是數字就算. 但 NaN(Not-a-Number)是數字喔.
  • boolean 只有兩個 true / false.
  • string 就是字串.
  • null 表示為空值的關鍵字; 也是基本型別.
  • undefined 連空值都不是的 undefined; 也是基本型別.
  • symbol 新的, 在 ECMAScript 2015(ES6) 才有. 這裡不討論.
除此之外的全都可以視為是物件(object). 其中有容易被誤解的資料包裝型別, 分別是
  • Number
  • Boolean
  • String
這不是跟上面的重複了? 錯, 請注意第一個字母是大寫. 這三個分別是numberboolean 和 string 的包裝物件. 證據如下,
> typeof 1
//'number'
> typeof new Number(1)
//'object'
>
> typeof '1'
//'string'
> typeof new String('1')
//'object'
>
> typeof true
//'boolean'
> typeof new Boolean(true)
//'object'
使用 new 來實例化基本資料型別的包裝物件, 再簡單的使用 typeof 檢查他的型別後, 可以看出他們都是物件而不是基本資料型別.
這就是第一個誤解. 有相同值的基本資料型別包裝物件會等於(===)該值.
> var str1='1'
//'1'
> var str2='1'
//'1'
> str1===str2
//true
// but 
> var strobj=new String('1')
> str1===strobj
//false
// 因為 str1 跟 strobj 型別不一樣
這是一個混用基本資料型別包裝物件跟基本資料型別時, 會出現的微妙問題, 強烈建議不要混用. 比較兩者, 使用 new 時會需要額外的資源來初始化, 且當需要 String 的方法時, js 會將基本資料型別當成型別包裝物件來使用. 所以就是說"不要再用 new String()new Number(),new Boolean() 了".
但是, 接下來就是
第二個誤會了. js 在使用 == 時會自動轉成相同型別來比較. 或 有相同基本資料值的基本資料型別包裝物件會相等
嚴格來說, 會轉型比較的只有 基本資料型別. 而如果比較的兩邊是 object 則是比較其內部參照(指標). 但在使用 == 比較時, 如果與 基本資料型別 比較的不是基本資料型別, js 會先呼叫 valueOf() 後再比較. 產生誤解的原因還是來自型別包裝物件. 型別包裝物件的原型繼承自Object , 所有 object 都會有 “valueOf” 這個方法. 根據定義, “The valueOf() method returns the primitive value of the specified object.”, 這本該傳回代表該物件的基本型別值. 但奇特的是, 實際上除了 基本資料型別包裝物件 會傳回基本型別值和 date會傳回 number 外, 其他都是傳回物件本身. 也就是, 基本資料型別包裝物件在與基本資料型別比較時會先透過 valueOf 傳回基本資料型別, 如果兩者型別還是不同, 則再將其轉型為相同型別來比較.
> var str1='1'
> var strobj=new String('1')
> str1==strobj
//true
// but
> String.prototype.valueOf=function(){ return 'this is string';}
// 修改 String 的 valueOf 方法
> str1==strobj
//false
// 因為 '1' != 'this is string'
> strobj.valueOf()
//'this is string'
這邊衍伸一個想法, 如果我想讓 object 的 valueOf 傳回可比較的值, 是不是 object 就能進行比較了? 遺憾, 物件的比較就只是比較其內部參照是否相同, 除非, 是物件與基本資料型別的比較, 物件才會呼叫 valueOf 來比較.
假如不是與基本資料型別的比較,
> var strobj1=new String('1')
> var strobj2=new String('1')
> strobj1==strobj2
//false
// 因為 strobj1, strobj1 雖然都是 String 類別, 但還都是物件. 
> typeof strobj1
//'object'
> typeof strobj2
//'object'
所以就是說"不要再用 new String()new Number()new Boolean() 了". 因為很重要, 所以說兩遍.
第三個誤解是 js 在使用 == 時會自動轉型, 因為 '0'==0true''==0是 true, 所以 '0'=='' 是?.
在基本資料型別可以分為兩類, 一種是有包裝的, 一種是沒包裝的. 沒包裝的都一樣, 有包裝的各有各的問題.
有型別包裝物件的
  • number
  • string
  • boolean
無型別包裝物件的
  • null
  • undefined
先說沒包裝的,
> null==null
//true
> undefined==undefined
//true
> null==undefined
//true
// 所以說沒包裝的都一樣
>
> 0==null
//false
> 0==undefined
//false
> ''==null
//false
> ''==undefined
//false
> false==null
//false
> false==undefined
//false
// 但他跟有包裝的不一樣
// null, undefined 也不會自動轉型為 number, string, boolean
講完沒包裝的, 剩下就是問題了.
學過數學的都知道, 單位相同才能比較. 如果有三種基本資料型別要兩兩比較, 有幾種轉型方法? 學過排列組合的都知道是 3*2 種. 但在 js 中到底是依什麼規則自動轉型, 隨機, 左邊優先, 右邊優先? 都不是. 使用 == 時自動轉型規則是型別 不同 就先轉成數字.
  • 先說 boolean 吧, 他只有 true / false 兩個值, 所以規則也很簡單. true 是 1 而 false 是 0.
  • 再來是 string, 凡是數字字串的都能轉成數字, 空字串(length=0)或空白字串(/\s+/)是0, 其他都是 NaN.
  • 然後 number 不轉型, 但 NaN 不等於 NaN.
剩下的就只是單純的數字比較罷了.
> false==0
//true
//
> true==1
//true
//
> '0'==0
//true
//
> ''==0
//true
//
> ' \t\r\n\v\f'==0
//true
//
> NaN==NaN
//false
//
> '0'==''
//false
// 兩個都是字串, 所以不轉型, 直接比較
關於轉型為 number, 有一個簡單的驗證方法是, 使用一元運算子+. 注意, 不是 二元 運算子 +. 也就是說在+之前不能有其他的運算元(operand).
> +''
//0
//
> +'0'
//0
//
> +' '
//0
//
> +false
//0
//
> +'NaN'
//NaN
// 字串 'NaN' 無法轉型為數字, 故為 NaN
> +null
//0
// 在 `==` null 是不轉型的
> +undefined
//NaN
// 在 `==` 只有 number, string, boolean 會自動轉型
一元運算子 + 的轉型規則如下,
  • 如果是 undefined 或 null, 則為 0
  • 物件會轉換成字串
  • 如果可能, 字串會轉成數字. 如果不行, 就是 NaN
  • 布林值會被當做數字, false 是 0, true 是 1
值得注意的是在單值的真假值判斷時空字串("", 長度為 0)是 false, 但空白字串(/\s+/, 正規表示式判斷為真時)是 true; 但在等於(==)判斷時, 空字串與空白字串都是 false.
> ' '==false
//true
// 因為 ' ' 轉成數字是 0, false 轉成數字也是 0
最後, 在 object 與基本資料型別比較時, object 會呼叫 object.valueOf() 方法, 以其傳回值與基本資料型別比較. 但若object.valueOf() 傳回的不是基本資料型別, 而是物件時, 則呼叫 object.toString()方法, 以其傳回值與基本資料型別比較.
以陣列來說, 會得到一些奇妙的結果.
> []==false
//true
//
> [,]==false
//true
//
> [,,]==false
//false
> [,,].toString()
//','
//
> [[],]==false
//true
//
> [[[]],]==false
//true
//
> [,[]]==false
//false
> [,[]].toString()
//','
//
> [[],[]]==false
//false
> [[],[]].toString()
//','
//
> [null,]==false
//true
//
> [null,[]]==false
// false
> [null,[]].toString()
//','
//
> [null,null]==false
//false
> [null,null].toString()
//','
// 以上的 false 全換成 0 或 "" 結果也一樣
// 
> [0,]==false
//true
//
> ['0',]==false
//true
//
> [1,]==true
//true
> [1,]==1
//true
> [2,]==true
//false
這表還能列的更長, 為了不要犯上奇怪的錯誤, 建議拿物件跟 numberbooleanstring 比較時, 請自行將物件轉為相同的型別, 別依賴自動轉型. 不然, 就把[結論]記清楚吧.

補充:

雖然 date 在等於比較(==)時是轉成字串來比較. 但在檢查是不是有效的日期上, 還是比較建議用 isNaN 來檢查.
> new Date('date')==NaN
//false
> new Date('date')=='NaN'
//false
> (+(new Date('date')))==NaN
//false
// 雖然使用`+`將日期轉成數字, 但無效日期是傳回 `NaN`, 
// 但 `NaN` 是不等於 `NaN` 的,
> (+(new Date('date')))=='NaN'
//false
// `NaN` 是 `number`, 所以右邊 'NaN' 字串要轉成數字, 可是他不是合法的數字字串, 
// 所以傳回的是 `NaN`, 然而 `NaN` 是不等於 `NaN` 的.
> new Date('date')=='Invalid Date'
//true
> isNaN(new Date('date'))
//true
如果不是特意要使用 == 的特性來處理, 在不確定結果時, 寧願多寫幾行使用先作型別檢查是否相等的 ===, 也不要有問題時, 找不到問題點.

沒有留言:

張貼留言