判断数组类型的终极大法(Array)

@一棵菜菜  May 3, 2018

怎么判断数组是前端日常开发、面试经常遇到的问题,数组也是最难以准确判断的类型之一。所以我总结了下如何判断数组。

typeof

红皮书p72

typeof 操作符是确定一个变量是字符串、数值、布尔值,还是 undefined 的最佳工具。


var b = true;
var u;
var o = new Object();
function test(){};

alert(typeof 'hello');// 'string'
alert(typeof 2);// 'number'
alert(typeof b);// 'boolean'
alert(typeof u);// 'undefined'

// 注意:null 不是 object
alert(typeof null);// 'object'

alert(typeof o);// 'object'
alert(typeof {});// 'object'
alert(typeof [1,2]); // 'object' 无法区别出来是否是数组类型

alert(typeof test);// function

typeof 的弊端

如果变量的值是一个对象或数组或 null,则 typeof 操作符会返回"object",无法实际区别。
虽然在检测基本数据类型时 typeof 是非常得力的助手,但在检测引用类型的值时,这个操作符的用处不大。通常,我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。为此,ECMAScript 提供了 instanceof 操作符。

instanceof

红皮书p72

instanceof 是 JavaScript 中判断是否继承的运算符,语法如下:

var result = variable instanceof constructor

如果变量是给定引用类型的实例 (根据它的原型链来识别) ,那么 instanceof 操作符就会返回 true。请看下面的例子:

alert(person instanceof Object); // 变量 person 是 Object 吗? 
alert(colors instanceof Array); // 变量 colors 是 Array 吗? 
alert(pattern instanceof RegExp); //变量pattern是RegExp吗?
// 组合继承
function Animal(){}
function Cat(){Animal.call(this);}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

// 或寄生组合继承
function Animal(){}
function Cat(){Animal.call(this);}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

var c = new Cat();
c instanceof Cat; // true
c instanceof Animal; // true
c instanceof Object; // true

根据规定,所有引用类型的值都是 Object 的实例。因此,在检测一个引用类型值和 Object 构造 函数时,instanceof 操作符始终会返回 true。

故可以通过 instanceof 判断数组:

function isArray(obj) {
  return obj instanceof Array;
}

instanceof 的弊端

instanceof 操作符的问题在于,它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array 构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。

var iframe = document.createElement('iframe');
document.body.append(iframe);
var FrameArray = window.frames[window.frames.length-1].Array;
var array = new FrameArray();
console.log(array instanceof Array);
// false

为了解决这个问题,ECMAScript 5 新增了 Array.isArray()方法。。

Array.isArray

语法如下:

Array.isArray(obj)

弊端

遗憾的是,Array.isArray 是 es5 的方法,并不兼容所有浏览器,ie9 以下浏览器都不支持5。

支持 Array.isArray()方法的浏览器有 IE9+、Firefox 4+、Safari 5+、Opera 10.5+和 Chrome。要
在尚未实现这个方法中的浏览器中准确检测数组,请参考红皮书 22.1.1 节。

2009 年,Prototype.js 维护者 kangax 发现可以用 Object.prototype.toString 判断数组,终于给数组的判断画上了句号。

Object.prototype.toString【准确检测数组】

Object.prototype.toString 的规则如下:

When the toString method is called, the following steps are taken:

If the this value is undefined, return “[object Undefined]”.
If the this value is null, return “[object Null]”.
Let O be the result of calling ToObject passing the this value as the argument.
Let class be the value of the [[Class]] internal property of O.
Return the String value that is the result of concatenating the three > > Strings “[object “, class, and “]”.

而数组的 [[class]] 值是 "Array":

15.4.2.1 new Array ( [ item0 [ , item1 [ , … ] ] ] )

The [[Class]] internal property of the newly constructed object is set to “Array”.

由此可知,Object.prototype.toString 可用来判断数组:

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

官方文档说明

toString()方法是定义在Object.prototype(Object的原型对象)上的,所以被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

此方法给不支持 Array.isArray 的浏览器提供了判断数组的方式,结合原生方法,我们可以得到适用性比较强的 isArray:

var isArray = Array.isArray || function(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
};

区别下面内容

var num = 123;
var val = num.toString(); // '123'

每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。

经历的三个步骤:
(1) 创建 Number 类型的一个实例;
(2) 在实例上调用指定的方法;
(3) 销毁这个实例。

// 可以将以上三个步骤想象成是执行了下列 ECMAScript 代码。  
var num = new Number(123);// 创建一个对应的基本包装类型的对象
var val = num.toString();// 是 Number.prototype.toString() 原型上的方法
num = null;
查看官方文档说明
Number 对象覆盖了 Object 对象上的 toString() 方法,它不是继承的 Object.prototype.toString()。对于 Number 对象,toString() 方法以指定的基数返回该对象的字符串表示。

对比其他类库

// jQuery
jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
    class2type[ "[object " + name + "]" ] = name.toLowerCase();
});
jQuery.type = function( obj ) {
    if ( obj == null ) {
        return obj + "";
    }
    return typeof obj === "object" || typeof obj === "function" ?
        class2type[ toString.call(obj) ] || "object" :
        typeof obj;
};
jQuery.isArray = Array.isArray || function( obj ) {
    return jQuery.type(obj) === "array";
};
// YUI
var TYPES = {
    'undefined'        : 'undefined',
    'number'           : 'number',
    'boolean'          : 'boolean',
    'string'           : 'string',
    '[object Function]': 'function',
    '[object RegExp]'  : 'regexp',
    '[object Array]'   : 'array',
    '[object Date]'    : 'date',
    '[object Error]'   : 'error'
};
L.type = function(o) {
    return TYPES[typeof o] || TYPES[TOSTRING.call(o)] || (o ? 'object' : 'null');
};
L.isArray = L._isNative(Array.isArray) ? Array.isArray : function (o) {
    return L.type(o) === 'array';
};
// Prototype.js
var ARRAY_CLASS = '[object Array]';
function isArray(object) {
    return _toString.call(object) === ARRAY_CLASS;
}
var hasNativeIsArray = (typeof Array.isArray == 'function')
    && Array.isArray([]) && !Array.isArray({});
if (hasNativeIsArray) {
    isArray = Array.isArray;
}
// underscore.js
var toString = Object.prototype.toString;
_.isArray = nativeIsArray || function(obj) {
    return toString.call(obj) == '[object Array]';
};
// lodash.js
var isArray = nativeIsArray || function(value) {
    return value && typeof value == 'object' && typeof value.length == 'number' &&
        toString.call(value) == arrayClass || false;
};

其中,jQuery,Prototype.js 和 lodash.js 对数组判断比较严格,加了其他限制。其他的类库和咱们的一样。

说明通过 Array.isArray 结合 Object.ptototype.toString 来判断数组,基本不会有问题。

iframe Object.prototype.toString or Array.isArray

JavaScript 中,Object.prototype 的方法都可以被重写,假如想想极端情况,一个熊孩子重写了 Object.prototype.toString,又重写了 Array.isArray 那岂不是上面的所有类库判断数组的方法都失效了?

经过测试确实如此,还好我们还有办法补救,以下即为 终极判断数组 方法:

function isArray() {
    document.body.append(document.createElement('iframe'));
    var frame = window.frames[window.frames.length-1];
    var FrameArray = frame.Array;
    var FrameObject = frame.Object;
    return FrameArray.isArray || function(obj) {
        return FrameObject.prototype.toString.call(obj) === '[object Array]';
    }
}

通过创建一个新的 iframe,保证原生方法没有被重写,确实绝妙。

不过,实际情况下不会有熊孩子闲着蛋疼去修改 Object.prototype.toString 和 Array.isArray 的,所以此种方法判断数组,有点画蛇添足了,仅作为茶余饭后的思考。

createElement 也被复写了
熊孩子:老湿,这个 iframe 方法确实厉害,但是假如我把 document.createElement 也复写了,怎么办呢?

老湿:熊孩子滚远点!

我的补充

Object.prototype.toString.call(true);// "[object Boolean]"
Object.prototype.toString.call(1);// "[object Number]"
Object.prototype.toString.call('hello'); // "[object String]"
var a;
Object.prototype.toString.call(a)  // "[object Undefined]"
Object.prototype.toString.call(null)  // "[object Null]"

Object.prototype.toString.call({a:1}) // "[object Object]"
Object.prototype.toString.call([1])  // "[object Array]"
var a=function(){};
Object.prototype.toString.call(a); // "[object Function]"

我的小结

  1. typeof():适合检测基本数据类型,但是不适合检测引用类型的值,如 typeof null/[]/{} 结果都为object
  2. instanceof:是判断是否继承的运算符,如果变量是给定引用类型的实例 (根据它的原型链来识别) ,那么 instanceof 操作符就会返回 true。但不适合判断跨框架传递来的数组。
  3. Array.isArray(): ES5的(IE9 以下浏览器都不支持)。
  4. Object.prototype.toString.call(obj) === '[object Array]':原生JS,浏览器都支持。
  5. 对于较旧的浏览器,可以安装Lodash库,并使用其isArray方法。如:_.isArray([1, 2, 3]);
// 自写判断数组的终极大法(浏览器基本上都能支持啦)
var isArray = Array.isArray || function(obj) {
    return Object.prototype.toString.call(obj) === '[object Array]';
};
// 使用
isArray([1,2,3]); // true
官方文档
本文大部分来自红皮书,或转自http://blog.xcatliu.com/2015/11/03/isarray/
我的其他相关文章《在 JavaScript 中如何检查对象为空》

添加新评论