JavaScript 准确判断数组

JavaScript 中判断数组看起来有很多方式:

[].constructor === Array
[].__proto__ === Array.prototype
Reflect.getPrototypeOf([]) === Array.prototype
Object.prototype.toString.call([]) === '[object Array]'
[].pop.toString() === 'function pop() { [native code] }'
[] instanceof Array
Array.isArray([])

正常情况下以上所有方法都是可以用来判断的, 但只有最后一种最准确. 其他方法都没法完全适用. 为什么这么说呢?

  1. 使用 constructor 或者 proto 或者 getPrototypeOf 方法

    实例化后的数组有一个 constructor 属性指向 Array. 这里稍微补充一个点, 在浏览器环境中, [].__proto__ === [].constructor.prototype === Array.prototype. 因此我们也可以通过 [].__proto__ === Array.prototype 来判断. 但 __proto__ 已经是一个已经被废弃的属性了, 不建议使用. 取而代之我们可以用 Reflect 来处理, 即 Reflect.getPrototypeOf([]) === Array.prototype. 它们是等效的. 那么用上述方法判断有什么问题呢?

    我们举个反例:

    const foo = Object.create(null)
    foo.constructor = Array
    console.log(foo.constructor === Array) // true
    foo.__proto__ = Array.prototype
    console.log(foo.__proto__ === Array.prototype) // true
    Reflect.setPrototypeOf(foo, Array.prototype)
    console.log(Reflect.getPrototypeof(foo) === Array.prototype) // true

    也就是说, 这个方法的问题在于 constructor__proto__(对象原型) 可以被重写. 虽然一般不会有人这样去写代码, 但是我们应该知道这些方法判断是有隐患的.

  2. 使用 Object.prototype.toString.call 方法

    JavaScript 内建对象一般都存在几个 Symbol 值, 这些在前面关于 JavaScript 元编程的文章已经有提及. 在调用 Object.prototype.toString() 方法时, 它会检查传入的参数是否拥有 [Symbol.toStringTag] 这个属性并且其为字符串, 如果满足的话, 则会被拼接返回, 下面的例子我们让他返回了跟数组一样的结果.

    class Bar {
     get [Symbol.toStringTag] () {
       return 'Array'
     }
    }
    console.log(Object.prototype.toString.call(new Bar()) === '[object Array]') // true

    因此使用 Object.prototype.toString.call 来判断数组也不是稳妥的, 但在 Array.isArray 不受支持的环境下, 这个方法还是被较多的用来判断数组的.

  3. 使用 [].pop.toString()

    这种方法主要是利用了内建函数隐藏的特点. 但这个是完全可以被伪造的. 但这种伪造就比较刁钻了.

    const foo = {
     pop: {
       toString () {
         return 'function pop() { [native code] }'
       }
     }
    }
    console.log(foo.pop.toString() === [].pop.toString())    // true
  4. instanceof 方法

    该方法实际上是不断的查找左值的原型, 如果找到和右值匹配的, 则返回 true. 该方法大多数情况是没问题的, 但是有一个情况例外:

    例子来源: instanceof_vs_isArray

    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    xArray = window.frames[window.frames.length-1].Array;
    var arr = new xArray(1,2,3); // [1,2,3]
    
    // Correctly checking for Array
    Array.isArray(arr);  // true
    // Considered harmful, because doesn't work though iframes
    arr instanceof Array; // false

    也就是说, 不同的 windows 下的 Array 是不相等的. 同样的, 以下代码判断也不行:

    const newWindow = window.open()
    const foo = new newWindow.window.Array()
    console.log(foo instanceof Array) // false

    相同的, 在 cluster, worker 中也一样. instanceof 有其局限性.

  5. Array.isArray 方法

    这个方法才是判断数组的最佳方法. 在不支持 Array.isArray 的情况下, 一般会使用 toString.call 来作为 Polyfill. Array.isArray 是 ES5 标准, 古董浏览器可能不支持.


参考资料:

  1. Determining with absolute accuracy whether or not a JavaScript object is an array

标签: JavaScript

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

添加新评论

This page loaded in 0.001186 seconds