3.2 数组的扩展与修复
得益于Prototype.js的ruby式数组方法的侵略,让Jser()前端工程师大开眼界,原来对数组的操作也如此丰富多彩的。原来JavaScript的数组方法基于上就是栈与队列的那一套,像splice还是很晚加入的。让我们回顾一下它们的用法。
pop方法:出栈操作,删除并返回数组的最后一个元素。
push方法:入栈操作,向数组的末尾添加一个或更多元素,并返回新的长度。
shift方法:出队操作,删除并返回数组的第一个元素。
unshift方法:入队操作,向数组的开头添加一个或更多元素,并返回新的长度。
slice方法:切片操作,从数组中分离出一个子数组,功能类似于字符串的substring、slice、substr这三兄弟。此方法也常用于转换类数组对象为真正的数组。
sort方法:对数组的元素进行排序,有一个可选参数,为比较函数。
reverse方法:颠倒数组中元素的顺序。
splice方法:可以同时用于原数组进行增删操作,数组的remove方法就是基于它写成的。
concat方法:用于把原数组与参数合并成一个新数组,如果参数为数组,那么它会把其第一维的元素放入新数组中。因此我们可以利用它实现数组的平坦化操作与克隆操作。
join方法:把数组的所有元素放入一个字符串。元素通过指定的分隔符进行分隔。你可以想象成字符串的split的反操作。
在 ecma262v5 中,它把标准浏览器早已实现的几个方法进行了入户处理,从此我们可以安心使用forEach等方法,不用担心它们忽然被废弃掉了。
indexOf方法:定位操作,返回数组中第一个等于给定参数的元素的索引值。
lastIndexOf方法:定位操作,同上,不过是从后遍历。索引操作可以说是字符串的同名方法的翻版,存在就返回非负整数,不存在就返回−1。
forEach 方法:迭代操作,将数组的元素依次传入一个函数中执行。Ptototype.js 的对应名字为each。
map方法:收集操作,将数组的元素依次传入一个函数中执行,然后把它们的返回值组成一个新数组返回。Ptototype.js的对应名字为collect。
filter 方法:过滤操作,将数组的元素依次传入一个函数中执行,然后把返回值为 true 的那个元素放入新数组返回。在Prototype.js中,它有三个名字,select、filter、findAll。
some方法:只要数组中有一个元素满足条件(放进给定函数返回true),那么它就返回true。Ptototype.js的对应名字为any。
every方法:只有数组中的元素都满足条件(放进给定函数返回true),它才返回true。Ptototype.js的对应名字为all。
reduce方法:归化操作。将数组中的元素中归化为一个简单的数值。Ptototype.js的对应名字为inject。
reduceRight方法:归化操作,同上,不过是从后遍历。
由于许多扩展也基于这些新的标准化方法,因此我先给出IE6、IE7、IE8的兼容方案,全部在数组原型上修复它们。
Array.prototype.indexOf = function(item, index) { var n = this.length, i = ~~index; if (i < 0) i += n; for (; i < n; i++) if (this[i] === item) return i; return -1; } Array.prototype.lastIndexOf = function(item, index) { var n = this.length, i = index == null ? n - 1 : index; if (i < 0) i = Math.max(0, n + i); for (; i >= 0; i--) if (this[i] === item) return i; return -1; }
像forEach、map、filter、some、every这几个方法,在结构上非常相似,我们可以这样生成它们。
function iterator(vars, body, ret) { var fun = 'for(var ' + vars + 'i=0,n = this.length;i < n;i++){' + body.replace('_', '((i in this) && fn.call(scope,this[i],i,this))') + '}' + ret return Function("fn,scope", fun); } Array.prototype.forEach = iterator('', '_', ''); Array.prototype.filter = iterator('r=[],j=0,', 'if(_)r[j++]=this[i]', 'return r'); Array.prototype.map = iterator('r=[],', 'r[i]=_', 'return r'); Array.prototype.some = iterator('', 'if(_)return true', 'return false'); Array.prototype.every = iterator('', 'if(!_)return false', 'return true');
造轮子的同学要注意一下,数组中的空元素是不会在上述方法中遍历出来的。
[1, 2, , 4].forEach(function(e) { console.log(e) }); //依次打印出1,2,4,忽略第二、第三个逗号间的空元素
reduce与reduceRight是一组,我们可以利用reduce方法创建reduceRight方法。
Array.prototype.reduce = function(fn, lastResult, scope) { if (this.length == 0) return lastResult; var i = lastResult !== undefined ? 0 : 1; var result = lastResult !== undefined ? lastResult : this[0]; for (var n = this.length; i < n; i++) result = fn.call(scope, result, this[i], i, this); return result; } Array.prototype.reduceRight = function(fn, lastResult, scope) { var array = this.concat().reverse(); return array.reduce(fn, lastResult, scope); }
接着下来,我们看看主流库为数组增加了那些扩展吧,除去上述那些。
Prototype.js的数组扩展:eachSlice、detect、grep、include、inGroupsOf、invoke、max、 min、partition、pluck、reject、sortBy、zip、size、clear、first、last、compact、flatten、without、 uniq、intersect、clone、inspect。
Rightjs 的数组扩展:include、clean、clone、compact、empty、first、flatten、includes、last、max、merge、min、random、reject、shuffle、size、sortBy、sum、uniq、walk、without。
mootools的数组扩展:clean、invoke、associate、link、contains、append、getLast、getRandom、include、combine、erase、empty、flatten、pick、hexToRgb、rgbToHex。
EXT的数组扩展:contains、pluck、clean、unique、from、remove、include、clone、merge、intersect、difference、flatten、min、max、mean、sum、erase、insert。
Underscore.js的数组扩展:detect、reject、invoke、pluck、sortBy、groupBy、sortedIndex、first、last、compact、flatten、without、union、intersection、difference、uniq、zip。
qooxdoo的数组扩展:insertAfter、insertAt、insertBefore、max、min、remove、removeAll、removeAt、sum、unique。
Tangram的数组扩展:contains、empty、find、remove、removeAt、unique。
我们可以发现,Prototype.js那一套方法影响深远,许多库都有它的影子,全面而细节地襄括了各种操作,大家可以根据自己的需要与框架宗旨制定自己的数组扩展。我在这方面的考量如下,至少要包含平坦化、去重、乱序、移除这几个操作,其次是两个集合间的操作,如取并集、差集、交集。
下面是各种具体实现。
contains方法:判定数组是否包含指定目标。
function contains(target, item) { return target.indexOf(item) > -1 }
removeAt方法:移除数组中指定位置的元素,返回布尔表示成功与否。
function removeAt(target, index) { return !!target.splice(index, 1).length }
remove方法:移除数组中第一个匹配传参的那个元素,返回布尔表示成功与否。
function remove(target, item) { var index = target.indexOf(item); if (~index) return removeAt(target, index); return false; }
shuffle方法:对数组进行洗牌。若不想影响原数组,可以先拷贝一份出来操作。有关洗牌算法的情况,可以见这篇博文:http://bost.ocks.org/mike/shuffle/。
function shuffle(target) { var j, x, i = target.length; for (; i > 0; j = parseInt(Math.random() * i), x = target[--i], target[i] = target[j], target[j] = x) { } return target; }
random方法:从数组中随机抽选一个元素出来。
function random(target) { return target[Math.floor(Math.random() * target.length)]; }
flatten方法:对数组进行平坦化处理,返回一个一维的新数组。
function flatten(target) { var result = []; target.forEach(function(item) { if (Array.isArray(item)) { result = result.concat(flatten(item)); } else { result.push(item); } }); return result; }
unique方法:对数组进行去重操作,返回一个没有重复元素的新数组。
function unique(target) { var result = []; loop: for (var i = 0, n = target.length; i < n; i++) { for (var x = i + 1; x < n; x++) { if (target[x] === target[i]) continue loop; } result.push(target[i]); } return result; }
compact方法:过滤数组中的null与undefined,但不影响原数组。
function compact(target) { return target.filter(function(el) { return el != null; }); }
pluck方法:取得对象数组的每个元素的指定属性,组成数组返回。
function pluck(target, name) { var result = [], prop; target.forEach(function(item) { prop = item[name]; if (prop != null) result.push(prop); }); return result; }
groupBy方法:根据指定条件(如回调或对象的某个属性)进行分组,构成对象返回。
function groupBy(target, val) { var result = {}; var iterator = $.isFunction(val) ? val : function(obj) { return obj[val]; }; target.forEach(function(value, index) { var key = iterator(value, index); (result[key] || (result[key] = [])).push(value); }); return result; }
sortBy方法:根据指定条件进行排序,通常用于对象数组。
function sortBy(target, fn, scope) { var array = target.map(function(item, index) { return { el: item, re: fn.call(scope, item, index) }; }).sort(function(left, right) { var a = left.re, b = right.re; return a < b ? -1 : a > b ? 1 : 0; }); return pluck(array, 'el'); }
union方法:对两个数组取并集。
function union(target, array) { return unique(target.concat(array)); }
intersect方法:对两个数组取交集。
function intersect(target, array) { return target.filter(function(n) { return ~array.indexOf(n); }); }
diff方法:对两个数组取差集(补集)。
function diff(target, array) { var result = target.slice(); for (var i = 0; i < result.length; i++) { for (var j = 0; j < array.length; j++) { if (result[i] === array[j]) { result.splice(i, 1); i--; break; } } } return result; }
min方法:返回数组中的最小值,用于数字数组。
function min(target) { return Math.min.apply(0, target); }
max方法:返回数组中的最大值,用于数字数组。
function max(target) { return Math.max.apply(0, target); }
基于上这么多了,如果你想实现sum方法,可以使用reduce方法。我们再来抹平Array原生方法在各浏览器的差异,一个是IE6、IE7下unshift不返回数组长度的问题,一个splice的参数问题。unshift BUG很容易修复的,使用函数劫持。
if ([].unshift(1) !== 1) { var _unshift = Array.prototype.unshift; Array.prototype.unshift = function() { _unshift.apply(this, arguments); return this.length; //返回新数组的长度 } }
splice在一个参数的情况下,IE6、IE7、IE8默认第二个参数为零,其他浏览器为数组的长度,当然我们要以标准浏览器为准!最简单的修复如下。
if ([1, 2, 3].splice(1).length == 0) {//如果是IE6、IE7、IE8,则一个元素也没有删除 var _splice = Array.prototype.splice; Array.prototype.splice = function(a) { if (arguments.length == 1) { return _splice.call(this, a, this.length) } else { return _splice.apply(this, arguments) } } }
另一种方法是使用slice进行实现,因此slice对待第二个参数的方式与标准的splice一致!
Array.prototype.splice = function(x, y) { var a = arguments, s = a.length - 2 - y, r = this.slice(x, x + y); if (s > 0) { for (var i = this.length - 1, j = x + y; i >= j; --i) this[i + s] = this[i]; } else if (s < 0) { for (var i = x + y, j = this.length; i < j; ++i) this[i + s] = this[i]; this.length += s; } for (var i = 2, j = a.length; i < j; ++i) this[i - 2 + x] = a[i]; return r; }
或者干脆自己实现一个,不利用任何原生方法:
Array.prototype.splice = function(s, d) { var max = Math.max, min = Math.min, a = [], i = max(arguments.length - 2, 0), k = 0, l = this.length, e, n, v, x; s = s || 0; if (s < 0) { s += l; } s = max(min(s, l), 0); d = max(min(isNumber(d) ? d : l, l - s), 0); v = i - d; n = l + v; while (k < d) { e = this[s + k]; if (e !== void 0) { a[k] = e; } k += 1; } x = l - s - d; if (v < 0) { k = s + i; while (x) { this[k] = this[k - v]; k += 1; x -= 1; } this.length = n; } else if (v > 0) { k = 1; while (x) { this[n - k] = this[l - k]; k += 1; x -= 1; } } for (k = 0; k < i; ++k) { this[s + k] = arguments[k + 2]; } return a; }
一旦有了splice方法,我们也可以自行实现自己的pop、push、shift、unshift方法,因此你明白为什么这几个方法是直接修改原数组了吧?浏览器商的思路与我们一样,大概也是用splice方法来实现它们!
var _slice = Array.prototype.slice; Array.prototype.pop = function() { return this.splice(this.length - 1, 1)[0]; } Array.prototype.push = function() { this.splice.apply(this, [this.length, 0].concat(_slice.call(arguments))); return this.length; } Array.prototype.shift = function() { return this.splice(0, 1)[0]; } Array.prototype.unshift = function() { this.splice.apply(this, [0, 0].concat(_slice.call(arguments))); return this.length; }