学习underscore源码之实现数组扁平化、去重、交集、并集、差集

567 阅读4分钟

前言

学习的underscore.js 源码版本为1.13.1
这一节学习与数组有关的功能函数,涉及的函数包括flatten、uniq、intersection、union、difference

数组的扁平化

如何实现数组的扁平化?

1. 利用ES6的flat方法

ES6中数组新增了flat方法,会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
其中参数表示想要拉平的层数,默认为1。如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

var arr = [1, 2, [3, [4, 5, [6]]]];

arr.flat();    // [1, 2, 3, [4, 5, [6]]]

arr.flat(2);    // [1, 2, 3, 4, 5, [6]]

arr.flat(Infinity);   // [1, 2, 3, 4, 5, 6]

var arr1 = [1, 2, , 4, 5];
// 会将empty元素过滤掉
arr1.flat();  // [1, 2, 4, 5]

2. 利用栈/队列 + ES6 拓展运算符

// 也可以使用shift/unshift, 但使用pop/push速度更快
function flatten(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    const next = stack.pop();
    if (Array.isArray(next)) {
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  // 反转
  return res.reverse();
}

测试下:

// 利用underscore中range函数创建整数灵活编号的列表
function range(start, stop, step) {
  if (stop == null) {
    stop = start || 0;
    start = 0;
  }
  if (!step) {
    step = stop < start ? -1 : 1;
  }

  var length = Math.max(Math.ceil((stop - start) / step), 0);
  var range = Array(length);

  for (var idx = 0; idx < length; idx++, start += step) {
    range[idx] = start;
  }

  return range;
}

// 使用shift/unshift
function flatten1(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    const next = stack.shift();
    if (Array.isArray(next)) {
      stack.unshift(...next);
    } else {
      res.push(next);
    }
  }
  return res;
}

var arr = range(10000);
// 需要扁平化的数组
var list = [arr, [arr], [arr, [arr]]]

console.time('使用pop/push耗时');
flatten(list)
console.timeEnd('使用pop/push耗时');


console.time('使用shift/unshift耗时');
flatten1(list)
console.timeEnd('使用shift/unshift耗时');

测试结果
或者使用下面代码:

var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr) {
  while (arr.some(Array.isArray)) {
    arr = [].concat(...arr);
  }
  return arr;
}
flatten(arr);  // [1, 2, 3, 4, 5, 6] 

3. 使用reduce + concat + 递归

var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr, deep){
  return deep > 0 ? arr.reduce((prev, next) => prev.concat(Array.isArray(next) ? flatten(next, deep - 1) : next), []) : arr.slice();
}
flatten(arr, Infinity);  // [1, 2, 3, 4, 5, 6]     

4. 使用Generator函数

function* flatten(array, depth) {
  if (depth === undefined) {
    depth = 1;
  }

  for (const item of array) {
    if (Array.isArray(item) && depth > 0) {
      yield* flatten(item, depth - 1);
    } else {
      yield item;
    }
  }
}

const arr = [1, 2, [3, 4, [5, 6]]];
const flattened = [...flatten(arr, Infinity)];
// [1, 2, 3, 4, 5, 6]

5. 使用toString + split方法(只适用于都是数字组成的数组,不推荐

var arr = [1, 2, [3, [4, 5, [6]]]];
function flatten(arr){
    return arr.toString().split(',')
}
console.log(flatten(arr));

6. 正则(不推荐

function flatten(arr) {
  let str = JSON.stringify(arr);
  str = str.replace(/(\[|\])/g, '');
  str = '[' + str + ']';
  return JSON.parse(str); 
}

underscore.js中的_.flatten(array, [shallow])

先来看使用效果:

var list = [1, [2], [3, [[[4]]]]];
_.flatten(list);  // [1, 2, 3, 4]

// shallow为true则数组将只减少一维的嵌套
_.flatten(list, true);  // [1, 2, 3, [4]]

// 处理arguments
var result = (function(){ return _.flatten(arguments); }(1, [2], [3, [[[4]]]]));
console.log(result);  // [1, 2, 3, 4]

_.flatten([[1,2,3], [4, 5, 6], 5, 1, 3], true);  // [1, 2, 3, 4, 5, 6, 5, 1, 3]

如何实现?

/**
 * 
 * @param {array|arguments} input 需要展开的数组或arguments
 * @param {?boolean|number} depth 
 * @param {?boolean} strict strict === true,通常和 depth === true 配合使用 表示只展开一层,但是不保存非数组元素(即无法展开的基础类型)
 * @param {?array} output 输出结果
 */
function flatten$1(input, depth, strict, output) {
  output = output || [];
  // depth为undefined、false
  if (!depth && depth !== 0) {
    depth = Infinity;
  } else if (depth <= 0) {
    return output.concat(input);
  }
  var idx = output.length;
  for (var i = 0, length = input.length; i < length; i++) {
    var value = input[i];
    // 数组 或者 arguments且length 属性值是不大于 Number.MAX_SAFE_INTEGER 的自然数
    if (isArrayLike(value) && (isArray(value) || isArguments$1(value))) {
      if (depth > 1) {
        // 递归展开
        flatten$1(value, depth - 1, strict, output);
        idx = output.length;
      } else {
        var j = 0,
          len = value.length;
        // 将 value 数组的元素添加到 output 数组中
        while (j < len) output[idx++] = value[j++];
      }
    } else if (!strict) {
      output[idx++] = value;
    }
  }
  return output;
}

function flatten(array, depth) {
  return flatten$1(array, depth, false);
}

数组去重

如何实现数组去重?

1. 利用ES6的Set 和 Map

var arr = [1, 2, 1, 4, 1, 3, '1'];

function unique(arr) {
  // return [...new Set(array)]
  return Array.from(new Set(arr));
}

unique(arr);  // [1, 2, 4, 3, '1']

2. 利用双层for循环

var arr = [1, 2, 1, 4, 1, 3, '1'];

function unique(arr) {
  // 用于存放结果
  var result = [];
  var arrLength = arr.length;
  for (var i = 0; i < arrLength; i++) {
    for (var j = 0, resLen = result.length; j < resLen; j++) {
      if (arr[i] === result[j]) {
        break;
      }
    }
    // 如果array[i]是唯一的,那么执行完循环,j等于resLen
    if (j === resLen) {
      result.push(arr[i]);
    }
  }
  return result;
}

unique(arr);  // [1, 2, 4, 3, '1']

3. 使用indexOf

使用 indexOf 简化内层的循环:

var arr = [1, 2, 1, 4, 1, 3, '1'];

function unique(arr) {
  // 用于存放结果
  var result = [];
  var arrLength = arr.length;
  for (var i = 0; i < arrLength; i++) {
    var current = arr[i];
    if (result.indexOf(current) === -1) {
      result.push(current)
    }
  }
  return result;
}

unique(arr);  // [1, 2, 4, 3, '1']

4. 先排序再去重

先进行排序,然后就可以只判断当前元素与上一个元素是否相同,相同就说明重复,不相同就添加进 result

var arr = [1, 2, 1, 4, 1, 3, '1'];

function unique(arr) {
  // 用于存放结果
  var result = [];
  var arrLength = arr.length,
    // 先利用slice或者concat进行浅拷贝一份数组然后进行排序
    sortedArray = arr.slice().sort((a, b) => a - b),
    prev;
  for (var i = 0; i < arrLength; i++) {
    var current = sortedArray[i]; 
    // 如果是第一个元素或者相邻的元素不相同
    if (!i || prev !== current) {
      result.push(current)
    }
    prev = current;
  }
  return result;
}

unique(arr);  // [1, '1', 2, 3, 4]

自己实现

第一版实现

function uniq(array, isSorted) {
  var result = [];
  var seen = [];
  for (var i = 0, length = array.length; i < length; i++) {
    var value = array[i];
    if (isSorted) {
      if (!i || seen !== value) result.push(value);
      seen = value;
    } else if (result.indexOf(value) === -1) {
      result.push(value);
    }
  }
  return result;
}

var arr = [1, 2, 1, 4, 1, 3, '1']
uniq(arr);  // [1, 2, 4, 3, '1']

var sortedArr = [1, 1, 1, '1', 2, 3, 4]
uniq(sortedArr, true);  // [1, '1', 2, 3, 4]

第二版优化 支持传迭代函数

function uniq(array, isSorted, iteratee) {
  var result = [];
  var seen = [];
  for (var i = 0, length = array.length; i < length; i++) {
    var value = array[i],
        // 如果指定了迭代函数
        // 则对数组每一个元素进行迭代
        computed = iteratee ? iteratee(value, i, array) : value;
    if (isSorted && !iteratee) {
      if (!i || seen !== computed) result.push(value);
      seen = computed;
    } else if (iteratee) {
      if (seen.indexOf(computed) === -1) {
        seen.push(computed);
        result.push(value);
      }
    } else if (result.indexOf(value) === -1) {
      result.push(value);
    }
  }
  return result;
}

var arr1 = [1, 'A', 'a', '1', 1, 3];

uniq(arr1, false, function (item) {
  return typeof item == 'string' ? item.toLowerCase() : item
})
// [1, 'A', '1', 3]

underscore.js中的_.uniq(array, [isSorted], [iteratee])

返回 array去重后的副本, 使用 === 做相等测试. 如果您确定 array 已经排序, 那么给 isSorted 参数传递 true值, 此函数将运行的更快的算法(直接相邻元素进行比较). 如果要处理对象元素, 传递 iteratee函数来获取要对比的属性。
源码如下:

/**
 * 如果obj包含指定的item则返回true(使用===检测)。如果obj 是数组,内部使用indexOf判断。使用fromIndex来给定开始检索的索引位置。
 * @param {array|object} obj 
 * @param {*} item 包含项
 * @param {*} fromIndex  开始检索的索引位置
 * @param {*} guard 
 */
function contains(obj, item, fromIndex, guard) {
  // 如果是对象,返回 values 组成的数组
  if (!isArrayLike(obj)) obj = values(obj);
  // 如果没有指定该参数,则默认从头找起
  if (typeof fromIndex != 'number' || guard) fromIndex = 0;
  // 使用indexOf寻找
  return indexOf(obj, item, fromIndex) >= 0;
}




/**
 * 数组去重
 * @param {array} array 
 * @param {?boolean} isSorted 是否已经排序, 如果排序则直接相邻元素比较
 * @param {?function} iteratee 迭代函数
 * @param {?obj} context 暴露的 API 中没 context 参数
 */
function uniq(array, isSorted, iteratee, context) {
  // 处理参数
  // 没有传入 isSorted 参数
  if (!isBoolean(isSorted)) {
    context = iteratee;
    iteratee = isSorted;
    isSorted = false;
  }
  // 如果有迭代函数, 则根据 this 指向二次返回新的迭代函数
  if (iteratee != null) iteratee = cb(iteratee, context);
  var result = [];
  // 用来过滤重复值
  var seen = [];
  for (var i = 0, length = array.length; i < length; i++) {
    var value = array[i],
        // 如果指定了迭代函数
        // 则对数组每一个元素进行迭代
        computed = iteratee ? iteratee(value, i, array) : value;
    if (isSorted && !iteratee) {
      // 如果是第一个元素或者相邻的元素不相同
      if (!i || seen !== computed) result.push(value);
      seen = computed;
    } else if (iteratee) {
      // 如果 seen[] 中没有 computed 这个元素值
      if (!contains(seen, computed)) {
        seen.push(computed);
        result.push(value);
      }
    } else if (!contains(result, value)) {  // 迭代函数不存在
      result.push(value);
    }
  }
  return result;
}

数组的交集(去重)

LeedCode 349. 两个数组的交集
获取两数组的交集(去重)

var intersection = function(nums1, nums2) {
  return [...new Set(nums1.filter((num) => nums2.includes(num)))]
};

underscore.js中的 _.intersection(*arrays)

返回传入 arrays(数组)交集。结果中的每个值是存在于传入的每个arrays(数组)里。

_.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]);   // [1, 2]

实现:

// 获取一个对象上自有属性(不包括原型链上的)的 values 值
function values(obj) {
  var _keys = keys(obj);
  var length = _keys.length;
  var values = Array(length);
  for (var i = 0; i < length; i++) {
    values[i] = obj[_keys[i]];
  }
  return values;
}

// 返回传入 arrays(数组)交集。结果中的每个值是存在于传入的每个arrays(数组)里
// 注意: 结果为去重的
function intersection(array) {
  var result = [];
  var argsLength = arguments.length;
  for (var i = 0, length = array.length; i < length; i++) {
    var item = array[i];
    // 返回的 result 是去重的
    if (contains(result, item)) continue;
    var j;
    // 判断其他参数数组中是否都有 item 这个元素
    for (j = 1; j < argsLength; j++) {
      if (!contains(arguments[j], item)) break;
    }
    if (j === argsLength) result.push(item);
  }
  return result;
}

数组的并集

_.union(*arrays)
返回传入的 arrays(数组)并集:按顺序返回,返回数组的元素是唯一的,可以传入一个或多个 arrays (数组)

_.union([1, 2, 3], [101, 2, 1, 10], [2, 1]);    // [1, 2, 3, 101, 10]

如何实现?

// 先用 flatten 方法将传入的数组展开成一个数组,再使用 _.uniq 方法
var union = restArguments(function(arrays) {
  return uniq(flatten$1(arrays, true, true));
});

数组的差集

_.difference(array, *others)
返回的值来自array参数数组,并且不存在于other 数组

_.difference([1, 2, 3, 4, 5], [5, 2, 10]);    // [1, 3, 4]

如何实现?

var difference = restArguments(function(array, rest) {
  // 先用 flatten 方法将传入的数组展开成一个数组
  rest = flatten$1(rest, true, true);
  // 遍历 array 过滤
  return filter(array, function(value){
    // 如果 value 存在在 rest 中,则过滤掉
    return !contains(rest, value);
  });
});

参考资料:
github.com/lessfish/un…
github.com/mqyqingfeng…