2.12 范型编程与集合多态
范型编程是指在多种数据类型上提供可复用的一套算法与数据结构,这种语言机制的产生缘由在于算法通常能适配多种数据类型。范型编程一般从几种特定的数据类型实现开始入手,随后逐步将之抽象成为能够兼容更多数据类型的通用版本。
范型编程并没有对不同类型的数据做算法差异性处理,相反,被传入的每种数据类型必须实现算法中所约定的功能,这些功能被称为“接口需求”。
范型编程具有参数多态性,它是作用于范类型参数上的逻辑分支,相比之下,重载则需要对所有不同类型的参数分别创建一套处理逻辑。
范型编程的理念与函数式编程紧密相关,因为函数式编程的最佳使用场景常见于单一函数对应多种数据类型。
在多数语言中,范型编程关注的是如何让算法去兼容不同类型的集合,在JavaScript中,集合可以包含任意类型的数据结构,许多程序员为了让算法适配集合中的元素,采用了鸭子类型的代码设计方式,他们忘记了多数JavaScript的内建对象方法本身就是支持范型调用的,这些方法可以在多种数据类型上使用。
JavaScript中的集合有对象与数组两种类型,它俩的关键不同之处在于,对象采用的是键值对的组织方式,而数组中的内容则是按照次序依次排开,对象不能保证内容的顺序而数组可以,除此之外,它们的行为都极为相似。实现一个兼容所有传入集合类型的函数带来的复用价值是显而易见的。
大部分作用于数组上的函数同样也应当可以在对象上调用,举例来说,假设你想从一个数组或对象集合中获取随机值。
最为简单的方法是通过数字索引下标找到相应的随机元素,但当集合类型为对象时,需要使用重载的方式将对象转换为数组。
var toArray = function toArray(obj) {
var arr = [],
prop;
for (prop in obj) {
if (obj.hasOwnProperty(prop)) {
arr.push(prop);
}
}
return arr;
};
randomItem()函数的实现很简单,首先确认传入集合的类型是否为数组,如果不是则将之转换为数组,最后使用内置方法Math.random()获取数组中的随机元素并返回:
var randomItem = function randomItem(collection) {
var arr = ({}.toString.call(collection) !==
'[object Array]')
? toArray(collection)
: collection;
return arr[Math.floor(arr.length * Math.random())];
};
test('randomItem()', function () {
var obj = {
a: 'a',
b: 'b',
c: 'c'
},
arr = ['a', 'b', 'c'];
ok(obj.hasOwnProperty(randomItem(obj)),
'randomItem works on Objects.');
ok(obj.hasOwnProperty(randomItem(arr)),
'randomItem works on Arrays.');
});
测试用例验证被返回的元素是否存在于目标集合中。
不同于真正意义上的范型,上述代码将对象视为一种特殊的数据类型,并在内部采用了不同的逻辑实现。由于在JavaScript中数组是对象的子集,所以从理论上看,任何作用在对象之上的操作同样适用于数组,换句话说,在JavaScript中,对象中的大部分范型方法无需做额外的逻辑处理,就可以在数组上执行。
集合的多态性对于提升代码复用性与API一致性来说是一项非常有效的策略,在诸如jQuery、Underscore这样的类库中,大部分方法同时可以兼容对象与数组。
JavaScript 1.6版本中为数组与字符串引入了不少内置的范型方法,在支持1.6版本语法的JavaScript引擎中,你可以在字符串对象上使用诸如every()这样的数组方法。
var validString = 'abc',
invalidString = 'abcd',
validArray = ['a', 'b', 'c'],
invalidArray = ['a', 'b', 'c', 'd'],
isValid = function isValid(char) {
return validString.indexOf(char) >= 0;
};
test('Array String generics', function () {
ok(![].every.call(invalidString, isValid),
'invalidString is rejected.');
ok([].every.call(validString, isValid),
'validString passes.');
ok(![].every.call(invalidArray, isValid),
'invalidArray is rejected.');
ok([].every.call(validArray, isValid),
'validArray passes.');
});
同理,字符串对象的方法也可以作用于数字对象。
var num = 303;
test('String number generics', function () {
var i = ''.indexOf.call(num, 0);
ok(i === 1,
'String methods work on numbers.');
});