如何让一个非数组构造器骗过浏览器和框架

var eve = function() {
  this.setArray(this.makeArray(arguments));
  return this;
}
eve.toString = function() {
  return "function Array() { [native code] }"
}
eve.prototype = {
  setArray: function(elems) {
    this.length = 0;//设置length以及重排索引
    Array.prototype.push.apply(this, elems);
    return this;
  },
  makeArray: function(arr) {//把传入参数变成数组
    var ret = [];
    if( arr != null ){    var i = arr.length;
      //单个元素,但window, string、 function有 'length'的属性,加其它的判断
      if( i == null || arr.split || arr.setInterval || arr.call ){
        ret[0] = arr;
      }else{
        try{
          ret = Array.prototype.slice.call(arr)
        }catch(e){
          while( i ) ret[--i] = arr[i];//Clone数组
        }
      }
    }
    return ret;
  },
  splice: [].splice,
  // Douglas Crockford: Duck typing arrays
  sort: [].sort,
  // Most lib
  toString: function() { return '[object Array]' }
}

// Chrome
// b = new eve // => []
// Array.isArray(b) // => false
// Object.prototype.toString.call(b) // => [object Object]

// Firefox
// b = new eve // => [object Array]
// Array.isArray(b) // => true
// Object.prototype.toString.call(b) // => [object Array]

以上是简化了司徒正美的代码后得到的结果,但还不能让 Chrome 的 Object.prototype.toString.call(b) 变为 [object Array] ,以及 Array.isArray(b) 的结果为 true

查看 ECMAScript 5 ,发现 Array.isArray() 的实现是:

If the value of the [[Class]] internal property of arg is "Array", then return true.

在搜索如何篡改 [[Class]] internal property 时,听说这个东西在 ECMAScript 6 里会被移除,Array.isArray() 的实现方式会变成这样:

If arg is an exotic Array object, then return true.

发现好像这条路有点崎岖……

后来偶然记起 instanceof 运算符的工作方式,是通过判断 instance.__proto__ === Class.prototype 来得到结果的,所以就想看看通过 __proto__ 能不能找到答案。

最终找到了一个看似通用的方案:

Array.createConstructor = function() {
  var constructor = function() {
    var result = Array.apply(null, arguments)
    result.__proto__ = constructor.prototype
    return result
  }
  constructor.toString = function() {
    return "function Array() { [native code] }"
  }
  constructor.prototype.constructor = constructor
  return constructor
}

var eve = Array.createConstructor()

// Chrome
// b = new eve // => []
// Array.isArray(b) // => true
// Object.prototype.toString.call(b) // => [object Array]

// Firefox
// b = new eve // => [object Array]
// Array.isArray(b) // => true
// Object.prototype.toString.call(b) // => [object Array]

由于 __proto__ 已经被 ES6 规范化了,而且目前的主流浏览器都支持 __proto__ 属性,所以使用它应该就是比较合适的方案了。

由于老浏览器本身没有实现 Array.isArray() 接口,使用的一般都是通过判断 Object.prototype.toString.call 获得的字符串的方式,所以可以将两种办法混合使用,最终代码(代码未经老浏览器测试):

var polyfillPrototype = {
  setArray : function(elems) {
    this.length = 0;//设置length以及重排索引
    Array.prototype.push.apply(this, elems);
    return this;
  },
  makeArray : function( arr ) {//把传入参数变成数组
    var ret = [];
    if( arr != null ){    var i = arr.length;
      //单个元素,但window, string、 function有 'length'的属性,加其它的判断
      if( i == null || arr.split || arr.setInterval || arr.call ){
        ret[0] = arr;
      }else{
        try{
          ret = Array.prototype.slice.call(arr)
        }catch(e){
          while( i ) ret[--i] = arr[i];//Clone数组
        }
      }
    }
    return ret;
  },
  splice: [].splice,
  sort: [].sort,
  toString: function() {
    return '[object Array]'
  }
}

var createConstructor = function() {
  var protoChecker = new (function() {}),
      constructor

  if ('__proto__' in protoChecker) {
    constructor = function() {
      var result = Array.apply(null, arguments)
      result.__proto__ = constructor.prototype
      return result
    }
    constructor.prototype.constructor = constructor
  } else {
    constructor = function() {
      this.setArray(this.makeArray(arguments));
      return this;
    }
    constructor.prototype = polyfillPrototype
  }
  constructor.toString = function() {
    return "function Array() { [native code] }"
  }
  return constructor
}

var eve = createConstructor()

参考文章:

  • 《模拟jQuery实现类数组对象》: http://www.cnblogs.com/rubylouvre/archive/2009/11/29/1612865.html
  • 《Array Subtypes》: http://wiki.ecmascript.org/doku.php?id=strawman:array_subtypes
  • 《[译]JavaScript: __proto__》: http://www.cnblogs.com/ziyunfei/archive/2012/10/05/2710955.html
  • 《Annotated ECMAScript 5.1》: http://es5.github.io/
  • 《unofficial ECMAScript 6》: http://people.mozilla.org/~jorendorff/es6-draft.html