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

_Fatman發表於2021-02-09

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方法的由來↩︎

相關文章