深入瞭解JS型別判斷

前端開發部落格發表於2020-10-31

關注『開發部落格』公眾號,回覆 加群

JS中判斷資料型別的方式有很多

  • typeof
  • Object.prototype.toString
  • instanceof
  • Array.isArray

一、回顧

JS資料型別分為基本型別和引用型別。
基本型別

  • undefined
  • null
  • Number
  • String
  • Boolean
  • Symbol

引用型別

  • Object
  • Function

函式是一種特殊的物件,即可呼叫的物件。

二、typeof

2.1 語法

typeof操作符可以區分基本型別,函式和物件。

console.log(typeof null) // object
console.log(typeof undefined) // undefined
console.log(typeof 1) // number
console.log(typeof 1.2) // number
console.log(typeof "hello") // string
console.log(typeof true) // boolean
console.log(typeof Symbol()) // symbol
console.log(typeof (() => {})) // function
console.log(typeof {}) // object
console.log(typeof []) // object
console.log(typeof /abc/) // object
console.log(typeof new Date()) // object
  1. typeof有個明顯的bug就是typeof nullobject;
  2. typeof無法區分各種內建的物件,如Array, Date等。

2.2 原理

JS是動態型別的變數,每個變數在儲存時除了儲存變數值外,還需要儲存變數的型別。JS裡使用32位(bit)儲存變數資訊。低位的1~3個bit儲存變數型別資訊,叫做型別標籤(type tag)

.... XXXX X000 // object
.... XXXX XXX1 // int
.... XXXX X010 // double
.... XXXX X100 // string
.... XXXX X110 // boolean
  1. 只有int型別的type tag使用1個bit,並且取值為1,其他都是3個bit, 並且低位為0。這樣可以通過type tag低位取值判斷是否為int資料;
  2. 為了區分int,還剩下2個bit,相當於使用2個bit區分這四個型別:object, double, string, boolean
  3. 但是nullundefinedFunction並沒有分配type tag

如何識別Function

函式並沒有單獨的type tag,因為函式也是物件。typeof內部判斷如果一個物件實現了[[call]]內部方法則認為是函式。

如何識別undefined

undefined變數儲存的是個特殊值JSVAL_VOID(0-2^30)typeof內部判斷如果一個變數儲存的是這個特殊值,則認為是undefined

    #define JSVAL_VOID              INT_TO_JSVAL(0 - JSVAL_INT_POW2(30))

如何識別null

null變數儲存的也是個特殊值JSVAL_NULL,並且恰巧取值是空指標機器碼(0),正好低位bit的值跟物件的type tag是一樣的,這也導致著名的bug:

typeof null // object

很不幸,這個bug也不修復了,因為第一版JS就存在這個bug了。祖傳程式碼,不敢修改啊。

有很多方法可以判斷一個變數是一個非null的物件,之前遇到一個比較經典的寫法:

// 利用Object函式的裝箱功能
function isObject(obj) {
    return Object(obj) === obj;
}

isObject({}) // true
isObject(null) // false

三、Object.prototype.toString

一般使用Object.prototype.toString區分各種內建物件。

3.2 語法

console.log(Object.prototype.toString.call(1)); // [object Number],隱式型別轉換
console.log(Object.prototype.toString.call('')); // [object String],隱式型別轉換
console.log(Object.prototype.toString.call(null)); // [object Null],特殊處理
console.log(Object.prototype.toString.call(undefined)); // [object Undefined],特殊處理
console.log(Object.prototype.toString.call(true)); // [object Boolean],隱式型別轉換
console.log(Object.prototype.toString.call( {})); // [object Object]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
  1. 如果實參是個基本型別,會自動轉成對應的引用型別;

Object.prototype.toString不能區分基本型別的,只是用於區分各種物件;

  1. nullundefined不存在對應的引用型別,內部特殊處理了;

3.3 原理

內部屬性[[Class]]

每個物件都有個內部屬性[[Class]],內建物件的[[Class]]的值都是不同的(“Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, “String”),並且目前[[Class]]屬性值只能通過Object.prototype.toString訪問。

Symbol.toStringTag屬性

其實Object.prototype.toString內部先訪問物件的Symbol.toStringTag屬性值拼接返回值的。

var a = "hello"
console.log(Object.prototype.toString.call(a)); // "[object String]"

// 修改Symbol.toStringTag值
Object.defineProperty(String.prototype, Symbol.toStringTag, {
    get() {
        return 'MyString'
    }
})

console.log(Object.prototype.toString.call(a)); // "[object MyString]"

如果哪個貨偷偷修改了內建物件的Symbol.toStringTag屬性值,那Object.prototype.toString也就不能正常工作了。

3.4 Object.prototype.toString內部邏輯

綜上可以總結Object.prototype.toString的內部邏輯:

  1. 如果實參是undefined, 則返回"[object Undefined]";

  2. 如果實參是null, 則返回"[object Null]";

  3. 把實參轉成物件

  4. 獲取物件的Symbol.toStringTag屬性值subType

    • 如果subType是個字串,則返回[object subType]
    • 否則獲取物件的[[Class]]屬性值type,並返回[object type]

四、instanceof

4.1 語法

object instanceof constructorFunc

instanceof 操作符判斷建構函式constructorFuncprototype屬性是否在物件object的原型鏈上。

Object.create({}) instanceof Object // true
Object.create(null) instanceof Object // false

Function instanceof Object // true
Function instanceof Function // true
Object instanceof Object // true
  1. 作為型別判斷的一種方式,instanceof 操作符不會對變數object進行隱式型別轉換
"" instanceof String; // false,基本型別不會轉成物件
new String('') instanceof String; // true
  1. 對於沒有原型的物件或則基本型別直接返回false
1 instanceof Object // false
Object.create(null) instanceof Object // false
  1. constructorFunc必須是個物件。並且大部分情況要求是個建構函式(即要具有prototype屬性)
// TypeError: Right-hand side of 'instanceof' is not an object
1 instanceof 1

// TypeError: Right-hand side of 'instanceof' is not callable
1 instanceof ({})

// TypeError: Function has non-object prototype 'undefined' in instanceof check
({}) instanceof (() => {})

4.2 intanceof的缺陷

不同的全域性執行上下文的物件和函式都是不相等的,所以對於跨全域性執行上下文intanceof就不能正常工作了。

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <iframe src=""></iframe>
        <script type="text/javascript">
            var iframe = window.frames[0];
            var iframeArr = new iframe.Array();

            console.log([] instanceof iframe.Array) // false
            console.log(iframeArr instanceof Array)  // false
            console.log(iframeArr instanceof iframe.Array)  // true       
        </script>
    </body>
</html>

4.3 原理

Symbol.hasInstance函式

instanceof操作符判斷建構函式constructorFuncprototype屬性是否在物件object的原型鏈上。但是可以利用Symbol.hasInstance自定義instanceof操作邏輯。

var obj = {}

// 自定義Symbol.hasInstance方法
Object.defineProperty(obj, Symbol.hasInstance, {
    value: function() {
        return true;
    }
});

1 instanceof obj // true

當然了這個舉例沒有任何實際意義。只是說明下Symbol.hasInstance的功能。Symbol.hasInstance本意是自定義建構函式判斷例項物件的方式,不要改變instanceof 的含義。

原型鏈

4.4 instanceof內部邏輯

綜上可以梳理instanceof內部邏輯

object instanceof constructorFunc
  1. 如果constructorFunc不是個物件,或則是null,直接拋TypeError異常;

  2. 如果constructorFunc[Symbole.hasInstance]方法,則返回!!constructorFunc[Symbole.hasInstance](object )

  3. 如果constructorFunc不是函式,直接拋TypeError異常;

  4. 遍歷object的原型鏈,逐個跟constructorFunc.prototype屬性比較:

    • 如果object沒有原型,則直接返回false;
    • 如果constructorFunc.prototype不是物件,則直接拋TypeError異常。

五、內建的型別判斷方法

5.1 Array.isArray

ES5引入了方法Array.isArray專門用於陣列型別判斷。Object.prototype.toStringinstanceof都不夠嚴格

var arr = []
Object.defineProperty(Array.prototype, Symbol.toStringTag, {
    get() {
        return 'myArray'
    }
})

console.log(Object.prototype.toString.call(arr)); // [object myArray]
console.log(Array.isArray(arr)); // true

console.log(Array.prototype instanceof Array); // false
console.log(Array.isArray(Array.prototype)); // true

不過現實情況下基本都是利用Object.prototype.toString作為Array.isArray的polyfill:

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

六、內建物件的prototype屬性型別判斷

內建的物件Number, String, Boolean, Object, Function, Date, RegExp, Array都是各自型別物件的建構函式,並且他們的prototype屬性都是各自例項物件的原型。但是這些內建物件的prototype屬性又是什麼型別呢?

6.1 Number.prototype

Number.prototype也是個數字,類似Number(0),但是Number.prototype並不是Number的例項。

var prototype = Number.prototype

console.log(prototype == 0); // true

console.log(prototype instanceof Number); // false
console.log(Object.prototype.toString.call(protoype)); // [object Number]

6.2 String.prototype

String.prototype也是字串,類似"",但是String.prototype並不是String的例項。

var prototype = String.prototype

console.log(prototype == ''); // true
console.log(prototype instanceof String); // false
console.log(Object.prototype.toString.call(prototype)); // [object String]

6.3 Boolean.prototype

Boolean.prototype也是Boolean,類似false,但是Boolean.prototype並不是Boolean的例項。

var prototype = Boolean.prototype

console.log(prototype == false); // true
console.log(prototype instanceof Boolean); // false
console.log(Object.prototype.toString.call(prototype)); // [object Boolean]

6.4 Object.prototype

Object.prototype也是Object,類似Object.create(null)的值(原型為null的空物件),但是Object.prototype並不是Object的例項。

var prototype = Object.prototype

Object.getPrototypeOf(prototype); // null
console.log(prototype instanceof Object); // false
console.log(Object.prototype.toString.call(prototype)); // [object Object]

6.5 Function.prototype

Function.prototype也是Function,是個空函式,但是Function.prototype並不是Function的例項。

var prototype = Function.prototype

console.log(prototype()) // undefined
console.log(prototype instanceof Function); // false
console.log(Object.prototype.toString.call(prototype)); // [object Function]

6.6 Array.prototype

Array.prototype也是Array,是個空陣列,但是Array.prototype並不是Array的例項。

var prototype = Array.prototype

console.log(prototype instanceof Array); // false
console.log(Array.isArray(prototype)) // true
console.log(Object.prototype.toString.call(prototype)); // [object Array]

6.6 RegExp.prototype

RegExp.prototype並不是RegExp的例項。但是關於RegExp.prototypeRegExp還是物件存在相容性問題,有些瀏覽器下RegExp.prototype也是RegExp,並且是個總返回true的正則。

var prototype = RegExp.prototype
console.log(prototype.test('hello')) // true
console.log(prototype instanceof RegExp); // false
// Chrome v84返回"[object Object]", IE返回"[object RegExp]"
console.log(Object.prototype.toString.call(prototype)); // 

整理自gitHub筆記:

  1. JS-型別&型別判斷
  2. JS-操作符-instanceof

關注公眾號「前端開發部落格」,回覆1024,領取前端資料包

相關文章