如何让一个非数组构造器骗过浏览器和框架
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