[Effective JavaScript 笔记]第51条:在类数组对象上复用通用的数组方法

前面有几条都讲过关于Array.prototype的标准方法。这些标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array。

arguments对象

在22条中提到的函数arguments对象。它是一个类数组对象,并不是一个标准的数组,所以无法使用数组原型中的方法,因此无法使用arguments.forEach这样的形式来遍历每一个参数。这里我们必须使用call方法来对使用forEach方法。

function highlight(){
  [].forEach.call(arguments,function(widget){
     widget.setBackground('yellow');
  });
}

forEach是一个函数对象,所以它继承了Function.prototype对象中的call方法。这里就可以使用一个指定的对象作为函数内部this的绑定对象来调用它,并紧随任意数量的参数。
## NodeList对象
在Web平台,DOM的NodeList类是另一个类数组对象。类似的document.getElementsByTagName会返回一个NodeList类数组对象。这个对象也没有继承自Array.prototype.

类数组对象

怎样构建一个类数组对象?

  • 具有一个范围在0到2^32-1的整形length属性

  • length属性大于该对象的最大索引。索引是一个范围在0到2^32-1的整数,它的字符串表示是该对象中的一个key

实现上面这两点,就可以使一个对象与Array.prototype中任一方法兼容。
一个简单的对象字面量也可以用来创建一个类数组对象。

var arrayLike={0:'a',1:'b',2:'c',length:3};
var res=Array.prototype.map.call(arrayLike,function(s){
    return s.toUpperCase();
});
res;//['A','B','C']

字符串

也是可以表现为数组,因为它们是可索引,并且其长度也可以通过length属性获取。
因此,Array.protoype中的方法操作字符串时并不会修改原始字符串。

var res=Array.prototype.map.call('abc',function(s){return s.toUpperCase();});
res;//['A','B','C']

模拟数组

模拟数组的所有行为,归功于数组行为的两方面:

    • 将length属性值设为小于n的值会自动地删除索引值大于或等于n的所有属性

    • 增加一个索引值为n(大于或等于length属性值)的属性会自动地设置length属性为n+1

其中第2条规则不好实现,因为需要监控索引属性的增加以自动地更新length属性。

对于Array.prototype中的方法,这两条都不是必须的,因为在增加或删除索引属性的时候它们都会强制地更新length属性。
Array方法中只有一个不是通用的,即数组连接方法concat。该方法可以由任意的类数组接收者调用,但它会检查其参数[[Class]]属性。如果参数是一个真实的数组,那么concat会将该数组的内容连接起来作为结果;否则,参数将以一个单一的元素来连接。
例如,不能简单地连接一个以arguments对象作为内容的数组。

function namesColumn(){
  return ['Names'].concat(arguments);
}
namesColumn('张三','李四','王五');//["Names", Arguments[3]0: "张三"1: "李四"2: "王五"callee: namesColumn()length: 3Symbol(Symbol.iterator): values()__proto__: Object]

使concat方法将一个类数组对象视为真实数组,需要把类数组转换为真正的数组。使用slice对类数组对象进行转换。

function namesColumn(){
  return ['Names'].concat([].slice.call(arguments));
}
namesColumn('张三','李四','王五');//["Names", "张三", "李四", "王五"]

提示

  • 对于类数组对象,通过提取方法对象并使用其call方法来复用通用的Array方法

  • 任意一个具有索引属性和恰当length属性的对象都可以使用通用的Array方法