为啥 Array.isArray 判断数组最靠谱?
在 JavaScript 中,如何判断一个值是否为数组是很常见的需求,一般有以下几种方法。
1、方式一:JSON.stringify
可以通过 JSON.stringify 序列化字符串,然后判断字符串是否以 [" 开头,以 "] 结尾来判断是否为数组。这其实和使用 JSON.stringify(obj) === '{}' 判断一个对象是否是空对象的思路类似。
const arr = [];
console.log(JSON.stringify(arr).startsWith('[') && JSON.stringify(arr).endsWith(']')); // true
优点:
- 兼容性好。
缺点:
- 需要序列化性能较差。
- 无法处理循环引用的数据,会报错
TypeError: Converting circular structure to JSON。 - 判断代码也较为复杂。
const arr = [];
arr[0] = arr; // 添加循环引用
console.log(JSON.stringify(arr).startsWith('[') && JSON.stringify(arr).endsWith(']')); // 报错 TypeError: Converting circular structure to JSON
2、方式二:instanceof
instanceof 可以判断某个实例是否属于某种类型,它内部会通过原型链往上一级一级查找,直到找到和当前对象的原型相等的原型。
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false
但是原型链是可以被修改的,所以 instanceof 判断结果可能不准确。比如我们可以利用 Object.setPrototypeOf(target, newProto) 来修改原型。
const arr = {};
Object.setPrototypeOf(arr, Array.prototype);
console.log(arr instanceof Array); // true
还有一种情况,就是跨 iframe 进行判断时,instanceof 判断结果可能不准确。
<!DOCTYPE html>
<html lang="en">
<body>
<script>
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const iframeArr = new iframeArray();
console.log(iframeArr instanceof Array); // false
</script>
</body>
</html>
这里其实 iframeArr 是一个数组,但是 instanceof 判断结果却是 false。
优点:
- 简单直观。
缺点:
- 通过
Object.setPrototypeOf修改原型后判断不准确。 - 跨
iframe失效。
3、方式三:constructor 属性
每个实例对象都有一个 constructor 属性,指向它的构造函数,所以通过 constructor 属性可以判断一个实例是否属于某个类型。
console.log([].constructor === Array); // true
console.log({}.constructor === Array); // false
然而和原型一样,constructor 属性也是可以被修改的。
const arr = [];
arr.constructor = Object; // 修改 constructor
console.log(arr.constructor === Array); // false(被修改了导致判断错误)
优点:
- 简单直观。
缺点:
-
constructor可被修改,判断不准确。
4、方式四:Object.prototype.toString.call
Object.prototype.toString.call() 是 JavaScript 中一个非常强大的类型检测方法,它可以获取任意值的内部 [[Class]] 属性,并以字符串形式返回其类型。
const arr = [];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
它是一个检测数据类型的标准方法,它不仅可以检查数组 Array、对象 Object,基本数据类型和其它引用类型像日期 Date、 正则 regexp 等都可以判断出来,实际开发中通常会把它封装成一个通用的类型检测方法。
// 判断特定类型
function isType(value, type) {
return Object.prototype.toString.call(value) === `[object ${type}]`;
}
console.log(isType([], 'Array')); // true
console.log(isType({}, 'Object')); // true
console.log(isType(new Date(), 'Date')); // true
console.log(isType(/abc/, 'RegExp')); // true
但是在 es6 的 Symbol出现之后,可以通过 Symbol.toStringTag 属性来控制其返回值,也会导致其判断不准确。
const arr = {
[Symbol.toStringTag]: 'Array'
}
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true
Symbol.toStringTag 也可以支持设置为任意值。
const obj = {
[Symbol.toStringTag]: '111' // 可以设置为任意值
}
console.log(Object.prototype.toString.call(obj)); // [object 111]
优点:
- 简单直观。
- 不仅可以判断数组,基本数据类型和其它引用数据类型
Date、Regexp也可以。
缺点:
- 代码较长,并且可通过
Symbol.toStringTag修改判断结果。
5、方式五:Array.isArray(最推荐)
Array.isArray() 是 Array 的一个静态方法,可以判断一个值是否为数组。
const arr = [];
console.log(Array.isArray(arr)); // true
优点:
- 最可靠。
- 简单直观。
缺点:
- 兼容性问题,只支持
IE9+。
为什么 Array.isArray 是最靠谱的呢?
1. 通过判断内部存储的数据结构是否符合数组的数据结构定义。
数组是一种数据结构:
- 它是一种线性表数据结构,也就是它里面的数据是排成一条线一样的,它的数据只有前和后两个方向。除了数组,队列、栈、链接也是线性表数据结构。
- 连续的内存空间。就是说数据中的元素在内存中是连续存储的,正是因为这个特性,所以数据可以实现随机访问,也就是访问数据中的每个元素花的时间就是一样的,时间复杂度都是
O(1),查询数据特别快。
2. 它是一个原生方法,内部 C++ 编码实现,我们从 js 代码层面无法判断数据内部的存储结构,也就无法模拟实现。
小结
| 判断是否是数组的方法 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| Array.isArray() | 标准、跨环境、最可靠 | 兼容性问题,只支持IE9+ | ⭐⭐⭐⭐⭐ |
| Object.prototype.toString.call() | 最兼容、可靠 | 代码较长,并且可通过 Symbol.toStringTag 修改判断结果 | ⭐⭐⭐⭐ |
| instanceof | 简单直观 | 通过 Object.setPrototypeOf 修改原型后判断不准确,跨 iframe 失效 | ⭐⭐⭐ |
| constructor | 简单 | constructor 可被修改 | ⭐⭐ |
| JSON.stringify | 兼容性好 | 无法处理循环引用、性能差 | ⭐ |
在平时实际前端项目开发中,如果需要判断是否是数组,直接用 Array.isArray() 就好了,它内部通过 C++ 编码实现,判断其存储结构是否为数组,是最可靠的判断方法,如果需要封装一个通用的数据类型判断方法,可以采用 Object.prototype.toString.call()。