1. 程式人生 > >javascript中判斷資料型別的四種方法及typeof、instanceof、constructor、toString

javascript中判斷資料型別的四種方法及typeof、instanceof、constructor、toString

在 ECMAScript 規範中,共定義了 6種資料型別,分為 基本型別 和 引用型別 兩大類,如下所示:

基本型別(簡單型別):String、Number、Boolean、Undefined、Null 。由於其佔據空間固定,是簡單的資料段,為了便於提升變數查詢速度,將其儲存在棧中,即按值訪問。

引用型別(複雜型別):Object。由於其值的大小會改變,所以不能將其存放在棧中,否則會降低變數查詢速度,因此,其值儲存在堆(heap)中,而儲存在變數處的值,是一個指標,指向儲存物件的記憶體處,即按址訪問。引用型別除 Object 外,還包括 Function 、Array、RegExp、Date 等等。

鑑於 ECMAScript 是鬆散型別的,因此需要有一種手段來檢測給定變數的資料型別,對於這個問題,JavaScript 也提供了多種技術方案,但遺憾的是,不同的方案得到的結果也參差不齊。

下面介紹常用的四種方案,並對各個方案存在的問題進行簡單的分析。

1.typeof

typeof 是一個操作符,其右側跟一個一元表示式,並返回這個表示式的資料型別。返回的結果用該型別的字串(全小寫字母)形式表示,包括以下 6 種:numberbooleanstringobjectundefinedfunction


typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效 typeof true; //boolean 有效 typeof undefined; //undefined 有效 typeof null; //object 無效 typeof [] ; //object 無效 typeof new Function(); // function 有效 typeof new Date(); //object 無效 typeof new RegExp(); //object 無效

有些時候,typeof 操作符會返回一些令人迷惑但技術上卻正確的值:

  • 對於基本型別,除 null 以外,均可以返回正確的結果。
  • 對於引用型別,除 function 以外,一律返回 object 型別。
  • 對於 null ,返回 object 型別。
  • 對於 function 返回 function 型別。

其中,null 有屬於自己的資料型別 Null , 引用型別中的 陣列、日期、正則 也都有屬於自己的具體型別,而 typeof 對於這些型別的處理,只返回了處於其原型鏈最頂端的 Object 型別,沒有錯,但不是我們想要的結果。

2.instanceof

instanceof 是用來判斷 A 是否為 B 的例項,表示式為:A instanceof B,如果 A 是 B 的例項,則返回 true,否則返回 false。 在這裡需要特別注意的是:instanceof 檢測的是原型。
我們用一段虛擬碼來模擬其內部執行過程:

instanceof (A,B) = {
    var L = A.__proto__;
    var R = B.prototype;
    if(L === R) {
        //A的內部屬性__proto__指向B的原型物件
        return true;
    }
    return false;
}

從上述過程可以看出,當 A 的 __proto__ 指向 B 的 prototype 時,就認為 A 就是 B 的例項,我們再來看幾個例子:

[] instanceof Array; //true
{} instanceof Object;//true
new Date() instanceof Date;//true

function Person(){};
new Person() instanceof Person;

[] instanceof Object; //true
new Date() instanceof Object;//true
new Person instanceof Object;//true

我們發現,雖然 instanceof 能夠判斷出 [ ] 是Array的例項,但它認為 [ ] 也是Object的例項,為什麼呢?

我們來分析一下 [ ]、Array、Object 三者之間的關係:

從 instanceof 能夠判斷出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最終 Object.prototype.proto 指向了null,標誌著原型鏈的結束。因此,[]、Array、Object 就在內部形成了一條原型鏈:

這裡寫圖片描述

從原型鏈可以看出,[] 的 proto 直接指向Array.prototype,間接指向 Object.prototype,所以按照 instanceof 的判斷規則,[] 就是Object的例項。依次類推,類似的 new Date()、new Person() 也會形成一條對應的原型鏈 。因此,instanceof 只能用來判斷兩個物件是否屬於例項關係, 而不能判斷一個物件例項具體屬於哪種型別。

instanceof 操作符的問題在於,它假定只有一個全域性執行環境。如果網頁中包含多個框架,那實際上就存在兩個以上不同的全域性執行環境,從而存在兩個以上不同版本的建構函式。如果你從一個框架向另一個框架傳入一個數組,那麼傳入的陣列與在第二個框架中原生建立的陣列分別具有各自不同的建構函式。

為了解決這個問題, ECMAScript 5 新增了 Array.isArray() 方法。這個方法的目的是最終確定某個值到底是不是陣列,而不管它是在哪個全域性執行環境中建立的。這個方法的用法如下。

Array.isArray(obj)
if (Array.isArray(value)){
   //對陣列執行某些操作
}

如下面例項:

// 下面的函式呼叫都返回 true
Array.isArray([]);
Array.isArray([1]);
Array.isArray(new Array());
// 鮮為人知的事實:其實 Array.prototype 也是一個數組。
Array.isArray(Array.prototype); 

// 下面的函式呼叫都返回 false
Array.isArray();
Array.isArray({});
Array.isArray(null);
Array.isArray(undefined);
Array.isArray(17);
Array.isArray('Array');
Array.isArray(true);
Array.isArray(false);
Array.isArray({ __proto__: Array.prototype });

支援 Array.isArray()方法的瀏覽器有 IE9+、 Firefox 4+、 Safari 5+、 Opera 10.5+和 Chrome。

當檢測Array例項時, Array.isArray 優於 instanceof,因為Array.isArray能檢測iframes.

var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[window.frames.length-1].Array;
var arr = new xArray(1,2,3); // [1,2,3]

// Correctly checking for Array
Array.isArray(arr);  // true
// Considered harmful, because doesn't work though iframes
arr instanceof Array; // false

假如不存在 Array.isArray(),則在其他程式碼之前執行下面的程式碼將建立該方法。

if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

3.constructor

當一個函式 F被定義時,JS引擎會為F新增 prototype 原型,然後再在 prototype上新增一個 constructor 屬性,並讓其指向 F 的引用。如下所示:

這裡寫圖片描述

當執行 var f = new F() 時,F 被當成了建構函式,f 是F的例項物件,此時 F 原型上的 constructor 傳遞到了 f 上,因此 f.constructor == F

這裡寫圖片描述

可以看出,F 利用原型物件上的 constructor 引用了自身,當 F 作為建構函式來建立物件時,原型上的 constructor 就被遺傳到了新建立的物件上, 從原型鏈角度講,建構函式 F 就是新物件的型別。這樣做的意義是,讓新物件在誕生以後,就具有可追溯的資料型別

同樣,JavaScript 中的內建物件在內部構建時也是這樣做的:

這裡寫圖片描述

細節問題:

  • null 和 undefined 是無效的物件,因此是不會有 constructor 存在的,這兩種型別的資料需要通過其他方式來判斷。
  • 函式的 constructor 是不穩定的,這個主要體現在自定義物件上,當開發者重寫 prototype 後,原有的constructor 引用會丟失,constructor 會預設為 Object

    這裡寫圖片描述

為什麼變成了 Object?

因為 prototype 被重新賦值的是一個 { }, { } 是 new Object() 的字面量,因此 new Object() 會將 Object 原型上的 constructor 傳遞給 { },也就是 Object 本身。

4.toString

toString 是 Object 原型物件上的方法,使用 call 來呼叫該方法會返回呼叫者的型別字串,格式為 [object,xxx],xxx 是呼叫者的資料型別,包括:String、Number、Boolean、Undefined、Null、Function、Date、Array、RegExp、Error、HTMLDocument 等, 基本上,所有的資料型別都可以通過這個方法獲取到。

Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ;    // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window是全域性物件 global 的引用

需要注意的是,必須通過 call 或 apply 來呼叫,而不能直接呼叫 toString , 從原型鏈的角度講,所有物件的原型鏈最終都指向了 Object, 按照JS變數查詢規則,其他物件應該也可以直接訪問到 Object 的 toString方法,而事實上,大部分的物件都實現了自身的 toString 方法,這樣就可能會導致 Object 的 toString 被終止查詢,因此要用 call/apply 來強制呼叫Object 的 toString 方法。