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

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

## 前言 毫無疑問,Array.isArray是現如今JavaScript中判斷物件是否屬於Array型別的首選,但是我認為了解本文其餘的方法及其背後的原理與侷限性也是很有必要的,因為在JavaScript中的大多數引用型別並沒有像Array型別一樣提供一個isArray的判斷方法,此時使用其餘的方法舉一反三是很有必要的。 ## 鴨子模型 **當一隻動物走起路來像鴨子,叫起來也像鴨子,那它就是一隻鴨子。 當一個物件中包含Array型別中的屬性(例如‘splice’、‘join’或者‘length’)時,那它就屬於Array型別。** [prototypejs的1.6.0.3版本](https://github.com/prototypejs/prototype/releases/tag/1.6.0.3)就是使用的這個邏輯,程式碼如下: ```javascript isArray: function(object) { return object != null && typeof object == "object" && 'splice' in object && 'join' in object; } ``` 不過鴨子模式存在一個問題,當一隻動物走起路來像鴨子,叫起來也像鴨子時,它除了可能是鴨子外,還可能是‘唐老鴨’。 ![圖片](https://img2020.cnblogs.com/blog/1604228/202102/1604228-20210220114259689-913064202.jpg) 這就好比一個包含Array型別中的屬性‘**splice**’、‘**join**’兩個屬性的物件,它除了可能是Array型別外,還可能是Person型別。 ```javascript 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](https://github.com/jquery/jquery)中的**isArrayLike**方法,程式碼如下: ```javascript 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 === 0 和 typeof length === "number" && length > 0 && ( length - 1 ) in obj 兩條判斷邏輯皆是通過‘length’屬性來判斷物件是否符合‘**like Array**’。 ## instanceof關鍵字 關於instanceof關鍵字的內容在[深入瞭解typeof與instanceof的使用場景及注意事項](https://www.cnblogs.com/liujingjiu/p/14393105.html)一文中已經詳細闡述過,故在此只作簡單說明。通過instanceof關鍵字來判斷物件的原型鏈上是否存在函式Array的prototype屬性值。如果存在,則說明此物件為Array型別,反之則不然。 **不過instanceof也並不能完全可信,比如通過Symbol.hasInstance屬性可以影響instanceof的判斷結果:** ```javascript 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== ```html main
``` ==iframe.html== ```html iframe

iframe

``` **npx http-server開啟main.html後,得到結果:** ![在這裡插入圖片描述](https://img2020.cnblogs.com/blog/1604228/202102/1604228-20210220114300124-1445526692.png) **由此可得知:不同全域性變數下的同名建構函式並不是同一個函式,當資料和型別不在同一個全域性變數下時使用instanceof來判斷是不可行的。** ## Object.prototype.toString方法 **Object.prototype.toString是不會受到跨全域性變數影響的。** ==main.html== ```html main
``` ==iframe.html== ```html iframe

iframe

``` **npx http-server開啟main.html後,得到結果:** ![在這裡插入圖片描述](https://img2020.cnblogs.com/blog/1604228/202102/1604228-20210220114300329-1432418505.png) **不過使用Symbol.toStringTag會影響Object.prototype.toString的輸出。** ```javascript 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] ``` ==也可以:== ```javascript 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] ``` ==還可以這樣寫:== ```javascript 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。** ```javascript 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的判斷。** ```javascript 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](https://github.com/v8/v8)中的[array-isarray.tq](https://github.com/v8/v8/blob/master/src/builtins/array-isarray.tq)檔案。** ```cpp // 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 ``` ## 結尾 由於本人才疏學淺,如發現問題,希望能向本人指出,