[Effective JavaScript 笔记]第58条:区分数组对象和类数组对象
示例
设想有两个不同类的API。
第一个是位向量:有序的位集合
var bits=new BitVector();
bits.enable(4);
bits.enable([1,3,8,17]);
bits.bitAt(4);//1
bits.bitAt(8);//1
bits.bitAt(9);//0
enable方法被重载了,可以传入一个索引或索引的数组。
第二个类的API是字符串集合:无序的字符串集合
var set=new StringSet();
set.add('Hamlet');
set.add(['Rosencrantz','Guildenstern']);
set.add({'Ophelia':1,'Polonius':1,'Horatio':1});
set.contains('Polonius');//true
set.contains('Guildenstern');//true
set.contains('Falstaff');//false
其中add方法被重载了,除了可以接收字符串和字符串数组外,还可以接收一个字典对象。
函数重载
为了实现BitVerctor.prototype.enable方法,可以通过测试其他情况来避免如何判断一个对象是否为数组的问题。
BitVector.prototype.enable=function(x){
if(typeof x === 'number'){
this.enableBit(x);
}else{
for(var i=0,n=x.length;i< n;i++){
this.enableBit(x[i]);
}
}
};
这里只实现了索引及索引数组的判断,很容易实现。那StringSet.prototype.add方法怎么实现呢?这里需要区分数组和对象。在js中数组就是一个对象。这里真正想做的是分离数组对象和非数组对象。
这样的区分和js的灵活的类数组对象的概念是有争执的。任何对象都可被视为数组,只要它遵循正确的接口。而且也没有明确的方法来测试一个对象是否满足一个接口。可以尝试把具有length属性的对象视为数组,但这也会有错误出现,比如碰巧一个字典对象有length属性呢?
dimensions.add({
'length':1,
'height':1,
'width':1
});
使用不精确的启发探索法来确定接口是一个容易被误解和滥用的方法。猜测一个对象是否实现了结构类型有时被称为鸭子测试,这是不好的实践。因为对象没有明确的信息标记来表示它们实现的结构类型,并没有可靠的编程方法来检测信息。
重载两种类型意味着必须有一种方法来区分这两种情况。不可能检测一个值是否实现了一种结构性的接口。
规则-API绝不应该重载与其他类型有重叠的类型
instanceof
对于StringSet,开始就不要使用结构性的类数组接口。相反,我们应当选择一种类型,这种类型具有明确的定义的“标签”,可以表明用户真想将其作为一个数组。一个显而易见但不完美的选择是使用instanceof操作符来测试一个对象是否继承自Array.prototype。
StringSet.prototype.add=function(x){
if(typeof x === 'string'){
this.addString(x);
}else if(x instaceof Array){
x.forEach(function(s){
this.addString(s);
},this);
}
};
任何时候一个Array的实例,它的行为会像一个数组。但有时候,在一些可以允许多个全局对象的环境中,可能会有标准的Array构造函数和原型对象的多份副本。在浏览器中有这种情况,每个frame会有标准库的一份单独副本。当跨frame通信时,一个frame中的数组不会继承自另一个frame的Array.prototype。
Array.isArray
出于上面这种情况,ES5引入了Array.isArray函数,其用于测试一个值是否是数组,而不管原型继承。在ES标准中,该函数测试对象的内部[[Class]]属性值是否是Array。当需要测试一个对象是否是真数组,而不仅仅是类数组对象,Array.isArray方法比instanceof操作符更好。
StringSet.prototype.add=function(x){
if(typeof x === 'undefined'){
this.addString(x);
}else if(Array.isArray(x)){
x.forEach(function(s){
this.addString(s);
},this);
}else{
for(var key in x){
this.addString(key);
}
}
};
Object.prototype.toString
在不支持ES5的环境中,可以使用标准的Object.prototype.toString方法测试一个对象是否为数组。
var toString=Object.prototype.toString;
functin isArray(x){
return toString.call(x) === '[object Array]';
}
Object.prototype.toString函数使用对象内部的[[Class]]属性创建结果字符串,所以测试一个对象是否为数组时,它比instanceof操作符更准确。
注意:该版本的add方法存在不同的影响该API的使用者的行为。重载API的数组版本不接收随意的类数组对象。例如,你不能传入arguments对象并期待它视为数组。
function MyClass(){
this.keys=new StringSet();
//...
}
MyClass.prototype.update=function(){
this.keys.add(arguments);
};
Array.prototype.slice
这里会把arguments作为字典来对待。可以把arguments对象转化为真正的数组。
MyClass.prototype.update=function(){
this.keys.add([].slice.call(arguments));
};
当调用者传入多个参数时,会把它们转化为真正的数组,然后再使用add操作。当调用者想传递一个类数组对象给一个期望接收真正数组的API时,都需要做这种转换。可以在API的文档中标明各API接收哪些类型的参数。如上面的例子中,enable方法接收数字和类数组对象。add方法接收字符串、真数组和非数组对象。
提示
-
绝不重载与其他类型有重叠的结构类型
-
当重载一个结构类型与其他类型时,先测试其他类型
-
当重载其他对象类型时,接收真数组而不是类数组对象
-
文档标注你的API是否接收真数组或类数组
-
使用ES5提供的Array.isArray方法测试真数组
翻译的文章,版权归原作者所有,只用于交流与学习的目的。
原创文章,版权归作者所有,非商业转载请注明出处,并保留原文的完整链接。