JavaScript中判斷物件是否屬於Array型別的4種方法及其背後的原理與侷限性

_Fatman發表於2021-02-20

前言

毫無疑問,Array.isArray是現如今JavaScript中判斷物件是否屬於Array型別的首選,但是我認為了解本文其餘的方法及其背後的原理與侷限性也是很有必要的,因為在JavaScript中的大多數引用型別並沒有像Array型別一樣提供一個isArray的判斷方法,此時使用其餘的方法舉一反三是很有必要的。

鴨子模型

當一隻動物走起路來像鴨子,叫起來也像鴨子,那它就是一隻鴨子。
當一個物件中包含Array型別中的屬性(例如‘splice’、‘join’或者‘length’)時,那它就屬於Array型別。

prototypejs的1.6.0.3版本就是使用的這個邏輯,程式碼如下:

isArray: function(object) {
    return object != null && typeof object == "object" && 'splice' in object && 'join' in object;
}

不過鴨子模式存在一個問題,當一隻動物走起路來像鴨子,叫起來也像鴨子時,它除了可能是鴨子外,還可能是‘唐老鴨’。
圖片

這就好比一個包含Array型別中的屬性‘splice’、‘join’兩個屬性的物件,它除了可能是Array型別外,還可能是Person型別。

function isArray(object) {
    return object != null && typeof object == "object" && 'splice' in object && 'join' in object;
}

function Person(){
    /**
    code
    */
}

Person.prototype.splice = function(){
    /**
    code
    */
}

Person.prototype.join = function(){
    /**
    code
    */
}

let p = new Person();

let isArr = isArray(p);

console.log('isArray : ' + isArr);//isArray : true

鴨子模式更多用在判斷‘like Array’上,比如jquery中的isArrayLike方法,程式碼如下:

function isArrayLike( obj ) {

	var length = !!obj && obj.length,
		type = toType( obj );

	if ( typeof obj === "function" || isWindow( obj ) ) {
		return false;
	}

	return type === "array" || length === 0 ||
		typeof length === "number" && length > 0 && ( length - 1 ) in obj;
}

其中的 length === 0typeof length === "number" && length > 0 && ( length - 1 ) in obj 兩條判斷邏輯皆是通過‘length’屬性來判斷物件是否符合‘like Array’。

instanceof關鍵字

關於instanceof關鍵字的內容在深入瞭解typeof與instanceof的使用場景及注意事項一文中已經詳細闡述過,故在此只作簡單說明。通過instanceof關鍵字來判斷物件的原型鏈上是否存在函式Array的prototype屬性值。如果存在,則說明此物件為Array型別,反之則不然。

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

function Person(){
}

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

let p = new Person();

p instanceof 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>      
    </head>
    <body>
        <iframe src="./iframe.html"></iframe>
        <script type="text/javascript">
            window.onload = function(){
                console.log('document.domain : ' + document.domain);
                let mainArr = [1,2,3,4,5];
                console.log('mainArr instanceof Array : ' + (mainArr instanceof Array));//
                let iframe = document.documentElement.getElementsByTagName('iframe')[0];
                let iframeWin = iframe.contentWindow.window;
                let iframeArr = iframeWin.arr;
                console.log('iframeArr : ' + JSON.stringify(iframeArr));
                console.log('iframeArr instanceof Array : ' + (iframeArr instanceof Array));//
                console.log('iframeArr instanceof iframeWin.Array : ' + (iframeArr instanceof iframeWin.Array));//
            }
        </script>  
    </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>iframe</p>
        <script type="text/javascript">
            window.onload = function(){
                window.arr = [6,7,8,9,10];
            }
        </script>  
    </body>
</html>

npx http-server開啟main.html後,得到結果:

在這裡插入圖片描述

由此可得知:不同全域性變數下的同名建構函式並不是同一個函式,當資料和型別不在同一個全域性變數下時使用instanceof來判斷是不可行的。

Object.prototype.toString方法

Object.prototype.toString是不會受到跨全域性變數影響的。

main.html

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
        <title>main</title>      
    </head>
    <body>
        <iframe src="./iframe.html"></iframe>
        <script type="text/javascript">
            window.onload = function(){
                let toString = Object.prototype.toString;
                console.log('document.domain : ' + document.domain);
                let mainArr = [1,2,3,4,5];
                console.log('toString.call(mainArr) : ' + (toString.call(mainArr)));//
                let iframe = document.documentElement.getElementsByTagName('iframe')[0];
                let iframeWin = iframe.contentWindow.window;
                let iframeArr = iframeWin.arr;
                console.log('toString.call(iframeArr) : ' + (toString.call(iframeArr)));//
            }
        </script>  
    </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>iframe</p>
        <script type="text/javascript">
            window.onload = function(){
                window.arr = [6,7,8,9,10];
            }
        </script>  
    </body>
</html>

npx http-server開啟main.html後,得到結果:
在這裡插入圖片描述

不過使用Symbol.toStringTag會影響Object.prototype.toString的輸出。

let toString = Object.prototype.toString;

function Person(){

}

let p = new Person();

console.log('toString.call(p) : ' + toString.call(p));//toString.call(p) : [object Object]

Object.defineProperty(p,Symbol.toStringTag,{
    get(){
        return "Person";
    }
})

console.log('toString.call(p) : ' + toString.call(p));//toString.call(p) : [object Person]

也可以:

let toString = Object.prototype.toString;

function Person(){

}

let p = new Person();

console.log('toString.call(p) : ' + toString.call(p));//toString.call(p) : [object Object]

Object.defineProperty(Person.prototype,Symbol.toStringTag,{
    get(){
        return "Person";
    }
})

console.log('toString.call(p) : ' + toString.call(p));//toString.call(p) : [object Person]

還可以這樣寫:

let toString = Object.prototype.toString;

class Person{
    get [Symbol.toStringTag](){
        return 'Person';
    }
}

let p = new Person();

console.log('toString.call(p) : ' + toString.call(p));//toString.call(p) : [object Person]

Array.isArray方法

Array.isArray是可以修改的,因為它的writable屬性值為true。

Object.getOwnPropertyDescriptor(Array,'isArray');
//{writable: true, enumerable: false, configurable: true, value: ƒ}

Array.isArray = function(data){
    return null !== data && typeof data === 'object';
}

console.log(Array.isArray(window));//true

Array.isArray是不會受到跨全域性變數影響的,並且修改Symbol.toStringTag 也不會影響到Array.isArray的判斷。

let toString = Object.prototype.toString;

Object.defineProperty(Array.prototype,Symbol.toStringTag,{
    get(){
        return "Person";
    }
})

let arr = new Array();

console.log(Array.isArray(arr));//true

console.log('toString.call(arr) : ' + toString.call(arr));//toString.call(arr) : [object Person]

具體Array.isArray的判斷邏輯 我找到了v8中的array-isarray.tq檔案。

// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

namespace runtime {
  extern runtime ArrayIsArray(implicit context: Context)(JSAny): JSAny;
}  // namespace runtime

namespace array {
  // ES #sec-array.isarray
  javascript builtin ArrayIsArray(js-implicit context:
                                      NativeContext)(arg: JSAny): JSAny {
    // 1. Return ? IsArray(arg).
    typeswitch (arg) {
      case (JSArray): {
        return True;
      }
      case (JSProxy): {
        // TODO(verwaest): Handle proxies in-place
        return runtime::ArrayIsArray(arg);
      }
      case (JSAny): {
        return False;
      }
    }
  }
}  // namespace array

結尾

由於本人才疏學淺,如發現問題,希望能向本人指出,感謝。

相關文章