JavaScript 数组操作合集
作者:Seiya
时间:2019年08月20日
数组去重
原始去重方法
使用循环嵌套,最外层循环 array,里面循环 newArray,如果 array[i] 的值跟 newArray[j] 的值相等,就跳出循环,如果都不等于,说明元素是唯一的,这时候 j 的值就会等于 newArray 的长度,根据这个特点进行判断,将值添加进 newArray。这个方法的有点在于 兼容性 很好。
function unique(array) {
var newArray = []
for(var i = 0, arrayLen = array.length; i < arrayLen; i++) {
for(var j = 0, newLen = newArray.length; j < newLen; j++) {
if (array[i] === newArray[j]) {
break;
}
}
// 如果 array[i] 是唯一的,那么执行完循环,j 等于 newLen
if (j === newLen) {
newArray.push(array[i])
}
}
return newArray;
}
indexOf
我们也可以用 indexOf
简化内层的循环:
function unique(array) {
var newArray = []
for(var i = 0, len = array.length; i < len; i++) {
var current = array[i]
if (newArray.indexOf(current) === -1) {
newArray.push(current)
}
}
return newArray
}
排序后去重
先将要去重的数组使用 sort
方法排序后,相同的值就会被排在一起,然后我们就可以只判断当前元素与上一个元素是否相同,相同就说明重复,不相同就添加进新数组中 :
function unique(array) {
var newArray = []
var sortedArray = array.concat().sort()
var seen
for (var i = 0, len = sortedArray.length; i < len; i++) {
// 如果是第一个元素或者相邻的元素不相同
if(!i || seen !== sortedArray[i]) {
newArray.push(sortedArray[i])
}
seen = sortedArray[i]
}
return newArray
}
tips
如果我们对一个已经排好序的数组去重,这种方法效率肯定高于使用 indexOf。
unique API
根据一个参数 isSorted 判断传入的数组是否是已排序的,如果为 true,我们就判断相邻元素是否相同,如果为 false,我们就使用 indexOf 进行判断。
function unique(array, isSorted) {
var newArray = []
var seen = []
for (var i = 0, len = array.length; i < len; i++) {
var value = array[i]
if (isSorted) {
if (!i || seen !== array[i]) {
newArray.push(value)
}
seen = value
}
else if (newArray.indexOf(value) === -1) {
newArray.push(value)
}
}
return newArray
}
优化 unique API
尽管 unqique 已经可以试下去重功能,为了让这个 API 更加强大,来考虑下面一个需求:
字母的大小写视为一致,比如'a'和'A',保留一个就可以了!
思路一
先处理数组中的所有数据,比如将所有的字母转成小写,然后再传入 unique 函数;
思路二
省掉处理数组的这一遍循环,直接就在去重的循环中做;
function unique(array, isSorted, iteratee) {
var res = [];
var seen = [];
for (var i = 0, len = array.length; i < len; i++) {
var value = array[i];
var computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted) {
if (!i || seen !== computed) {
res.push(value)
}
seen = computed;
}
else if (iteratee) {
if (seen.indexOf(computed) === -1) {
seen.push(computed);
res.push(value);
}
}
else if (res.indexOf(value) === -1) {
res.push(value);
}
}
return res;
}
我们可以这样使用:
var array = [1, 1, 'a', 'A', 2, 2];
console.log(unique(array3, false, function(item){
return typeof item == 'string' ? item.toLowerCase() : item
}));
filter
我们可以使用 ES5 提供了 filter
方法,简化最外层循环:
- 使用 indexOf 方法:
function unique(array) {
var newArray = array.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return newArray
}
- 使用排序去重的方法:
function unique(array) {
return array.concat().sort().filter(function(item, index, array) {
return !index || item !== array[index - 1]
})
}
Object 键值对
利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的。示例如下:
function unique(array) {
var obj = {}
return array.filter(function(item, index, array) {
return obj.hasOwnProperty(item) ? false : (obj[item] = true)
})
}
这个代码是有问题的,因为 1 和 '1' 是不同的,但是这种方法会判断为同一个值,这是因为对象的键值只能是字符串,所以我们可以使用 typeof item + item 拼成字符串作为 key 值来避免这个问题:
function unique(array) {
var obj = {};
return array.filter(function(item, index, array){
return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
})
}
tips
filter 为数组中的每个元素调用一次 callback 函数,并利用所有使得" callback 返回 true 或 等价于 true 的值 的元素"创建一个新数组。
ES6
我们可以利用 ES6 中的新特性进行数组去重操作,比如:Set,它类似于数组,但是成员的值都是唯一的,没有重复的值。
function unique(array) {
return Array.from(new Set(array));
}
再简化一下:
function unique(array) {
return [...new Set(array)]
}
此外,如果用 Map 的话:
function unique (arr) {
const seen = new Map()
return arr.filter((a) => !seen.has(a) && seen.set(a, 1))
}
特殊类型去重比较
对于以下数组,各种方法得到的结果:
var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN];
方法 | 结果 | 说明 |
---|---|---|
for循环 | [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] | 对象和 NaN 不去重 |
indexOf | [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] | 对象和 NaN 不去重 |
sort | [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] | 对象和 NaN 不去重 数字 1 也不去重 |
filter + indexOf | [1, "1", null, undefined, String, String, /a/, /a/] | 对象不去重 NaN 会被忽略掉 |
filter + sort | [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] | 对象和 NaN 不去重 数字 1 不去重 |
优化后的键值对方法 | [1, "1", null, undefined, String, /a/, NaN] | 全部去重 |
Set | [1, "1", null, undefined, String, String, /a/, /a/, NaN] | 对象不去重 NaN 去重 |
求数组的最大值和最小值
Math.max 和 Math.min
JavaScript 提供了 Math.max 函数返回一组数中的最大值,用法是:
Math.max([value1[,value2, ...]])
注意:
- 如果有任一参数不能被转换为数值,则结果为 NaN。
- max 是 Math 的静态方法,所以应该像这样使用:Math.max(),而不是作为 Math 实例的方法 (简单的来说,就是不使用 new )
- 如果没有参数,则结果为 -Infinity (注意是负无穷大)
如果任一参数不能被转换为数值,这就意味着如果参数可以被转换成数字,就是可以进行比较的,比如:
Math.max(true, 0) // 1
Math.max(true, '2', null) // 2
Math.max(1, undefined) // NaN
Math.max(1, {}) // NaN
原始方法
function max(array) {
var result = array[0]
for(var i = 1; i < array.length; i++) {
result = Math.max(result, array[i])
}
return result
}
reduce
既然是通过遍历数组求出一个最终值,那么我们就可以使用 reduce 方法:
function max(prev, next) {
return Math.max(prev, next)
}
arr.reduce(max);
tips
reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,最终合并为一个值。
排序
arr.sort(function(a, b){ return a - b });
console.log(arr[arr.length - 1])
tips
sort的比较函数有两个默认参数,通常我们用 a 和 b 接收两个将要比较的元素:
- 若比较函数返回值<0,那么a将排到b的前面;
- 若比较函数返回值=0,那么a 和 b 相对位置不变;
- 若比较函数返回值>0,那么b 排在a 将的前面;
apply
var arr = [6, 4, 1, 8, 2, 11, 23];
console.log(Math.max.apply(null, arr))
ES6
var arr = [6, 4, 1, 8, 2, 11, 23];
console.log(Math.max(...arr))
数组扁平化
数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。
递归
function flatten(array) {
var result = []
for(var i = 0, len = array.length; i < len; i++) {
if (Array.isArray(array[i])) {
result = result.concat(flatten(array[i]))
} else {
result.push(array[i])
}
}
return result
}
toString
如果数组的元素都是数字,那么我们可以考虑使用 toString 方法:
[1, [2, [3, 4]]].toString() // "1,2,3,4"
调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗:
function flatten(arr) {
return arr.toString().split(',').map(function(item){
return + item
})
}
tips
map() 创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
reduce
既然是对数组进行处理,最终返回一个值,可以考虑使用 reduce 来简化代码:
function flatten(array) {
return array.reduce(function(prev, next) {
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}
ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
[].concat(...array)
用这种方法只可以扁平一层,但是顺着这个方法一直思考,所以我们可以进行一下优化:
function flatten(array) {
while(array.some(item => Array.isArray(item))) {
array = [].concat(...array)
}
return array
}
tips
some() 判断数组中的是否有满足判断条件的元素
undercore
那么如何写一个抽象的扁平函数,来方便我们的开发呢,这里我们可以直接使用 underscore 的实现,下面是它的源码:
/**
* 数组扁平化
* @param {Array} input 要处理的数组
* @param {boolean} shallow 是否只扁平一层
* @param {boolean} strict 是否严格处理元素(去掉非数组元素)
* @param {Array} output 这是为了方便递归而传递的参数
* 源码地址:https://github.com/jashkenas/underscore/blob/master/underscore.js#L528
*/
function flatten(input, shallow, strict, output) {
// 递归使用的时候会用到output
output = output || [];
var idx = output.length;
for (var i = 0, len = input.length; i < len; i++) {
var value = input[i];
// 如果是数组,就进行处理
if (Array.isArray(value)) {
// 如果是只扁平一层,遍历该数组,依此填入 output
if (shallow) {
var j = 0, length = value.length;
while (j < length) output[idx++] = value[j++];
}
// 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output
else {
flatten(value, shallow, strict, output);
idx = output.length;
}
}
// 不是数组,根据 strict 的值判断是跳过不处理还是放入 output
else if (!strict){
output[idx++] = value;
}
}
return output;
}
查找指定元素
findIndex
ES6 对数组新增了 findIndex 方法,它会返回数组中满足提供的函数的第一个元素的索引,否则返回 -1。
function isBigEnough(element) {
return element >= 15;
}
[12, 5, 8, 130, 44].findIndex(isBigEnough); // 3
实现 findIndex
function findIndex(array, predicate, context) {
for (var i = 0; i < array.length; i++) {
if (predicate.call(context, array[i], i, array)) return i;
}
return -1;
}
findLastIndex
findIndex 是正序查找,对应的 findLastIndex 就是倒序查找:
function findLastIndex(array, predicate, context) {
let len = array.length
for( let i = len - 1; i >= 0; i--) {
if(predicate.call(context, array[i], i, array)) return i
}
return -1
}
createIndexFinder
findIndex 和 findLastIndex 其实有很多重复的部分,如何精简冗余的内容呢?我们可以学习 underscore 的思路:利用传参的不同,返回不同的函数。
function createIndexFinder(dir) {
return function(array, predicate, context) {
var len = array.length
var index = dir > 0 ? 0 : len - 1
for(; index >= 0 && index < len; index += dir) {
if(predicate.call(context, array[index], index, array)) return index
}
return -1
}
}
var findIndex = createIndexFinder(1);
var findLastIndex = createIndexFinder(-1);
sortedIndex
在来看一个需求:在一个排好序的数组中找到 value 对应的位置,保证插入数组后,依然保持有序的状态。
既然是有序的数组,那我们就不需要遍历,可以使用二分查找法,确定值的位置。我们尝试着写一版:
function sortedIndex(array, obj) {
let low = 0, high = array.length
while(low < high) {
let mid = Math.floor((low + high) / 2)
if(array[mid] < obj) low = mid + 1
else high = mid
}
return high
}
这个方法虽然很有用,但是,只是针对数组,为了保证通用性,我们希望可以处理对象。所以我们得到第二版代码:
function cb(func, context) {
if (context === void 0) return func;
return function() {
return func.apply(context, arguments);
};
}
function sortedIndex(array, obj, iteratee, context) {
iteratee = cb(iteratee, context)
var low = 0, high = array.length;
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < iteratee(obj)) low = mid + 1;
else high = mid;
}
return high;
};
我们可以这样使用:
var stooges = [{name: 'zhangsan', age: 10}, {name: 'lisi', age: 30}];
var result = sortedIndex(stooges, {name: 'zhangsan', age: 20}, function(stooge){
return stooge.age
});
查询元素是否存在
indexOf、lastIndexOf
我们模仿 sortedIndex 的实现,尝试实现 indexOf 和 lastIndexOf 函数:
function createIndexOfFinder(dir) {
return function(array, item){
var length = array.length;
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
if (array[index] === item) return index;
}
return -1;
}
}
var indexOf = createIndexOfFinder(1);
var lastIndexOf = createIndexOfFinder(-1);
fromIndex
indexOf 方法也可以多传递一个参数 fromIndex,基于此我们得到第二版:
function createIndexOfFinder(dir) {
return function(array, item, idx){
var length = array.length;
var i = 0;
if (typeof idx == "number") {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(length + idx, 0);
}
else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
}
return -1;
}
}
var indexOf = createIndexOfFinder(1);
var lastIndexOf = createIndexOfFinder(-1);
优化
underscore 在此基础上还做了两点优化:
支持查找 NaN;
支持对有序的数组进行更快的二分查找;
function createIndexOfFinder(dir, predicate, sortedIndex) {
return function(array, item, idx){
var length = array.length;
var i = 0;
if (typeof idx == "number") {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(length + idx, 0);
}
else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
}
else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
// 如果该插入的位置的值正好等于元素的值,说明是第一个符合要求的值
return array[idx] === item ? idx : -1;
}
// 判断是否是 NaN
if (item !== item) {
idx = predicate(array.slice(i, length), isNaN)
return idx >= 0 ? idx + i: -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
}
return -1;
}
}
var indexOf = createIndexOfFinder(1, findIndex, sortedIndex);
var lastIndexOf = createIndexOfFinder(-1, findLastIndex);
数组遍历
尽管 ES5 提供了 forEach 方法,但是 forEach 没有办法中止或者跳出 forEach 循环,除了抛出一个异常。但是对于 jQuery 的 each 函数,如果需要退出 each 循环可使回调函数返回 false,其它返回值将被忽略。
首先,我们实现一个 each 函数,具体思路:根据参数的类型进行判断,如果是数组,就调用 for 循环,如果是对象,就使用 for in 循环,有一个例外是类数组对象,对于类数组对象,我们依然可以使用 for 循环。
function each(obj, callback) {
let len, i = 0
if( isArrayLike(obj)) {
length = obj.length;
for ( ; i < length; i++ ) {
callback(i, obj[i])
}
} else {
for ( i in obj ) {
callback(i, obj[i])
}
}
return obj
}
function isArrayLike(obj) {
// obj 必须有 length属性
var length = !!obj && "length" in obj && obj.length;
var typeRes = type(obj);
// 排除掉函数和 Window 对象
if (typeRes === "function" || isWindow(obj)) {
return false;
}
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
终止循环
我们只需要将:
callback(i, obj[i])
修改成:
if(callback(i, obj[i]) === false) {
break;
}
this
修改 this 指向问题,我们可以得到最终版:
function each(obj, callback) {
var length, i = 0;
if (isArrayLike(obj)) {
length = obj.length;
for (; i < length; i++) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
} else {
for (i in obj) {
if (callback.call(obj[i], i, obj[i]) === false) {
break;
}
}
}
return obj;
}