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判斷的是儲存單元中的標籤型別,所以不會受到影響。