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, ...]])

注意:

  1. 如果有任一参数不能被转换为数值,则结果为 NaN。
  2. max 是 Math 的静态方法,所以应该像这样使用:Math.max(),而不是作为 Math 实例的方法 (简单的来说,就是不使用 new )
  3. 如果没有参数,则结果为 -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;
}
最后更新时间: 2019-8-23 15:07:42