深入了解typeof與instanceof的使用場景及注意事項

JavaScript中的數據類型分為兩類,undefined,number,boolean,string,symbol,bigint,null[1]組成的基礎類型Object、Function、Array等類型組成的引用類型

在這裡插入圖片描述
如何判斷數據屬於哪種類型是JavaScript中非常重要的一個知識點,其中最常用的兩個方法就是分別使用typeof與instanceof這兩個關鍵字來對數據的類型進行判斷。

typeof與instanceof雖然都可以用來對數據所屬的類型進行判斷,但是它們之間還是存在差異的,而這種差異主要存在於兩個方面:
1.作用點的不同;
typeof主要用來判斷基礎數據類型,instanceof則是用來判斷引用數據類型。

2.底層邏輯的不同;
typeof是根據數據在存儲單元中的類型標籤來判斷數據的類型,instanceof則是根據函數的prototype屬性值是否存在於對象的原型鏈上來判斷數據的類型。

typeof判斷數據類型共有8個值,它們分別是『undefined』、『number』、『boolean』、『string』、『symbol』、『bigint』、『object』和『function』。

使用typeof就可以很好的判斷數據類型undefined、number、boolean、string、symbol和bigint。
不過在判斷基礎類型null時,使用typeof便不再準確了。這個問題的產生可以追溯到JavaScript的第一個版本[2],在這個版本中,單個值在棧中佔用32位的存儲單元,而這32位的存儲單元又可以劃分為類型標籤(1-3位)和實際數據,類型標籤存儲於低位中,具體可以分成5種:
1.當第0位、第1位和第2位皆為0時,typeof判斷此數據類型為』object』;
2.當第0位為1時,typeof判斷此數據類型為』number(整數)』;
3.當第0位與第2位皆為0,而第1位為1時,typeof判斷此數據類型為』number(浮點數)』;
4.當第0位與第1位皆為0,而第2位為1時,typeof判斷此數據類型為』string』;
5.當第1位與第2位皆為1,而第0位為0時,typeof判斷此數據類型為』boolean』;

此外還有兩種特殊情況:
undefined:整數−2^30 (整數範圍之外的數字)
null:第0位到第31位皆為0

當數據值為null時,正好滿足當第0位、第1位和第2位皆為0時,typeof判斷類型為』object』的條件,所以typeof null === ‘object’的結果為true。

使用typeof判斷function也是存在問題的:
在 IE 6, 7 和 8 上,很多宿主對象是對象而不是函數。
例如:

typeof alert === 'object';//true

還有老版本Firefox中的

typeof /[0-9]/ === 'function';//true

像這種的還有很多,就不一樣舉例了,多半是瀏覽器實現差異,現在已經統一標準了。

我在ie11上運行的結果:

typeof alert === 'function';//true

在當前最新版Firefox上運行的結果:

typeof reg === 'object';//true

typeof在判斷引用類型還存在一些問題,例如:

typeof {} === 'object';//true
typeof [] === 'object';//true
typeof window === 'object';//true
typeof new Map() === 'object';//true

這個時候如果想要知道更詳細的資訊就需要使用instanceof關鍵字了。

alert instanceof Function;//true
({}) instanceof Object;//true
([]) instanceof Array;//true
window instanceof Window;//true
(new Map()) instanceof Map;//true

使用instanceof運算符,我們可以清楚的判斷對象的原型鏈上是否存在函數的prototype屬性值。

不過instanceof也並不能完全可信,比如通過Symbol.hasInstance屬性可以影響instanceof的判斷結果:

function Person(){
}

Object.defineProperty(Person,Symbol.hasInstance,{
    value : function(){
        return false;
    }
})

let p = new Person();

p instanceof Person;//false

但是Symbol.hasInstance屬性並不會影響到數據的原型鏈,使用自定義的myInstanceof方法[3]不會受到Symbol.hasInstance屬性的影響:

/**
* obj 變數
* fn 構造函數
*/
function myInstanceof(obj,fn){
    let _prototype = Object.getPrototypeOf(obj);
    if(null === _prototype){
        return false;
    }
    let _constructor = _prototype.constructor;
    if(_constructor === fn){
        return true;
    }
    return myInstanceof(_prototype,fn);
}

function Person(){
}

Object.defineProperty(Person,Symbol.hasInstance,{
    value : function(){
        return false;
    }
})

let p = new Person();

p instanceof Person;//false

myInstanceof(p,Person);//true

自定義的myInstanceof方法改進版:

/**
* obj 變數
* fn 構造函數
*/
function myInstanceof(obj,fn){
    let _prototype = Object.getPrototypeOf(obj);
    if(null === _prototype){
        return false;
    }
    let _constructor = _prototype.constructor;
    if(_constructor[Symbol.hasInstance]){
        return _constructor[Symbol.hasInstance](obj);
    }
    if(_constructor === fn){
        return true;
    }
    return myInstanceof(_prototype,fn);
}

function Person(){
}

Object.defineProperty(Person,Symbol.hasInstance,{
    value : function(){
        return false;
    }
})

let p = new Person();

p instanceof Person;//false

myInstanceof(p,Person);//false

數據和類型不在一個全局變數下時instanceof也會輸出錯誤的結果

比方說現在定義兩個html文件,分別為main.html和iframe.html,程式碼如下:

main.html

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>main</title>      
        <script type="text/javascript">
            window.onload = function(){
                console.log('document.domain : ' + document.domain);
                let iframe = document.documentElement.getElementsByTagName('iframe')[0];
                let p = iframe.contentWindow.window.document.documentElement.getElementsByTagName('p')[0];
                console.log('p instanceof Object : ' + (p instanceof Object));//p instanceof Object : false
            }
        </script>  
    </head>
    <body>
        <iframe src="./.html"></iframe>
    </body>
</html>

iframe.html

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>iframe</title>       
    </head>
    <body>
        <p>1</p>
    </body>
</html>

npx http-server打開main.html後,得到結果p instanceof Object : false

圖片

造成這種結果原因在於:

iframe.contentWindow.window.Object === window.Object;//false

那瀏覽器要為什麼這樣呢?都用一個Object構造函數不好嗎?

我們這樣看:

main.html打開一個window(我們現在叫它main_window),iframe.html打開一個window(我們現在叫它iframe_window)。
我們現在從iframe_window中獲取一個p元素對象,它的原型鏈為—HTMLParagraphElement.prototype -> HTMLElement.prototype -> Element.prototype -> Node.prototype -> EventTarget.prototype -> Object.prototype
然後我們在main.html文件中,去判斷p instanceof Object,也就是判斷 p instanceof main_window.Object。p元素是在iframe.html文件中被構造的,所以p instanceof iframe_window.Object === true。如果想讓p instanceof main_window.Object === true
那麼要滿足iframe_window.Object === main_window.Object。但是這個條件肯定是不能滿足的。如果iframe_window.Object === main_window.Object,那麼我在iframe.html文件中修改Object函數,就會作用到main.html中,這樣會引發很嚴重的安全的問題,以及一系列莫名其妙的bug。

所以不同的全局變數下的同名構造函數並不是同一個函數,這導致了instanceof在數據與函數位於不同全局變數下時會判斷出錯

不過在這個例子使用typeof倒是可以解決問題,只是要記住判斷數據是不是null類型:

null !== p && typeof p === 'object';//true

因為typeof判斷的是存儲單元中的標籤類型,所以不會受到影響。

參考


  1. 一般基礎類型我是用小寫字母開頭。 ↩︎

  2. 此時還沒有Symbol、BigInt,故不在討論範圍內。 ↩︎

  3. myInstanceof方法的由來↩︎