深入理解 JavaScript 中的型別和型別判斷問題

aniiantt發表於2018-10-18

也許你曾被 js 中的型別判斷搞的暈頭轉向的,下面我們來從頭梳理一下 js 中的型別判斷問題。

基本型別

JavaScript 中的基本型別有 6 種:

  • null
  • undefined
  • boolean
  • number
  • string
  • symbol

還有 object。我們可以通過 typeof 判斷它們的型別,如:

typeof null === 'object' // true
typeof undefined === 'undefined' // true
typeof 11 === 'number' // true
...
複製程式碼

其中 typeof null === 'object',所以並不能單純的通過 typeof 判斷 null ,這是 js 實現的問題。如果想判斷是否是 null 的話,可以這樣做:

var a = null
// 複合判斷
(!a && typeof a === 'object') // true
// 判斷相等性
a === null // true
複製程式碼

還有一種情況就是,對於 function,它是 object 的子型別,但是 typeof function 會返回 function。

typeof function(){} === 'function' // true
複製程式碼

由於 function 是一個 object,所以 function 可以擁有普通物件屬性。它的內部有一個 [[Call]] 屬性,使得它可以被呼叫。

但是判斷 Array,就不能這樣了,雖然 Array 也是 Object 的子型別。

typeof [] // object
複製程式碼

對於一個變數,如果它沒有值的時候, typeof 會返回 undefined,否則則會返回它的值的型別。對於 undefined 我們也可以直接判斷相等性。

var a
a === undefined // true
複製程式碼

但是 undefined 是可以被賦值改寫的。為了防範這種情況,我們通過 void 運算子,進行操作,void __ 總會返回 undefined

var a
a === void 0 // true
複製程式碼

對於一個未宣告(undeclared)的變數,直接訪問會報錯,但是 typeof 依然會返回 undefined

nnnnnnnn // Uncaught ReferenceError: nnnnnnnn is not defined
typeof nnnnnnnn // undefined
複製程式碼

這個特性可以幫助我們做一些防禦性程式設計,因為直接 if(xxxxxx) 會拋錯,但是 if(typeof xxxxxx !== 'undefined') 不會。

寬鬆相等和嚴格相等

你應該會經常聽到很多規範要求你不要用 == 而用 ===,那麼這兩個相等之間有什麼區別呢。簡單的說來就是 == 會發生隱式型別轉化,而 === 不會。

下面是 == 隱式型別轉換的規則:

github.com/aniiantt/kn…

深入理解 JavaScript 中的型別和型別判斷問題

www.ecma-international.org/ecma-262/5.…

== 有時候是會帶來一些便利的,比如說 null 和 undefined 比較是 true。字串和數字比較字串會被轉換數字再比較。

比較令人費解的地方是布林值和其他型別比較,布林值先被轉換為數字 0 或者 1,再進行比較。物件和非物件之間比較,物件會被先轉換為原始值。

或許你以為 == 就應該照你想的那樣工作,沒有符合你的預期的話,你會覺得是坑。但其實不是這樣的,它有著一套完善的規則。我的意見是,如果你十分清楚瞭解隱式轉換的規則的話,== 是完全可以用的,你知道自己在幹什麼。否則使用 === 。

特殊的值和數字判斷

NaN 表示一個無效的數字,它依然是數字型別:

typeof NaN == 'number' // true
複製程式碼

如果你簡單的通過 == 或 NaN 判斷一個數字是否是 NaN:

NaN === NaN // false
NaN !== NaN // true
複製程式碼

這樣是不行的!window 有一個全域性的函式 isNaN 可以用來判斷是否是 NaN,它的檢查方式是判斷一個值是否不是一個 NaN,也不是數字。也就是說:

isNaN('aaa') // true
isNaN(NaN) // true
isNaN(100) // false
isNaN('100') // false
複製程式碼

ES6 新增 Number.isNaN 會判斷一個數字型別是否是 NaN:

Number.isNaN('aaa') // false
Number.isNaN(NaN) // true
Number.isNaN(100) // false
Number.isNaN('100') // false
複製程式碼

除此之外還有一個 Infinity -- 無窮大。 1 / 0 就是 ∞,Infinity。同理 isFinite 可以判斷是否不是 Infinity,它同樣有一個全域性函式和一個 Number 型別的成員函式:

isFinite('aaa') // false
isFinite(NaN) // false
isFinite(Infinity) // false
isFinite('100') // true
isFinite(100) // true
Number.isFinite('aaa') // false
Number.isFinite(NaN) // false
Number.isFinite(100) // false
Number.isFinite('100') // false
Number.isFinite(100) // true
複製程式碼

對於零值,我們來看看一下一個例子:

0 * -1 // -0
複製程式碼

+0 -0 的存在是在數學上合理的。但在程式中判斷它們並不是那麼容易的,。好在 ES6 提供了 Object.is 函式,我們可以用它來處理這些特殊值:

Object.is(0, -0) // false
Object.is(-0, -0) // false
Object.is(NaN, NaN) // true
Object.is(Infinity, Infinity) // true
Object.is(Infinity, -Infinity) // false
複製程式碼

詳細見 developer.mozilla.org/zh-CN/docs/…

瞭解知道了這麼多特殊值,那麼我們該怎麼判斷

內建型別的判斷

如 RegExp Function Date Array 它們是 js 引擎自帶的函式,那麼該怎麼判斷它們的型別呢。

它們內部實現,有一個 [[class]] 屬性,通過 Object.prototype.toString(...),可以間接訪問到這個屬性,從而實現型別判斷:

Object.prototype.toString.call(/./) // [object RegExp]
Object.prototype.toString.call(() => {}) // [object Function]
Object.prototype.toString.call(new Date()) // [object Date]
Object.prototype.toString.call([]) // [object Array]
複製程式碼

你也可以使用 instanceof 進行判斷,如 [] instanceof Array === true 但是請注意,這樣在跨宿主環境時會有問題的(如跨 iframe),因為兩個不同環境的 Array 物件並不是同一個。

類和物件

a instanceof Foo 可以用來判斷 a 的整條原型鏈 [[prototype]] 中,是否有指向 Foo.prototype 的引用:

function Foo() {}; Foo.prototype.a = '';
function Bar() {}; Bar.prototype = new Foo(); Bar.prototype.b = '';
new Bar instanceof Foo // true
new Bar instanceof Bar // true
複製程式碼

但是如果你是採用 Object.create(...) 的方式建立原型鏈,那麼 instanceof 就不是那麼管用了,因為此時並不存在什麼建構函式。

a.isPrototypeOf(b) 可以判斷 a 是否出現在 b 的原型鏈中。 a instanceof Foo 可以用 Foo.prototype.isPrototypeOf(a) 代替,這樣更清晰的顯示了他們的關係。

Promise 中是通過檢查返回結果是否具有 then 方法,來判斷返回結果是否是一個 thenable 這樣做很方便,但同時也很脆弱。我們不得不注意要避免給物件加上 then 屬性,不然你在 Promise 中返回這個物件的時候,會被當作一個 Promise,即使它不是。

參考資料

相關文章