Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript专题之数组扁平化 #36

Open
mqyqingfeng opened this issue Jul 21, 2017 · 53 comments
Open

JavaScript专题之数组扁平化 #36

mqyqingfeng opened this issue Jul 21, 2017 · 53 comments

Comments

@mqyqingfeng
Copy link
Owner

mqyqingfeng commented Jul 21, 2017

扁平化

数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。

举个例子,假设有个名为 flatten 的函数可以做到数组扁平化,效果就会如下:

var arr = [1, [2, [3, 4]]];
console.log(flatten(arr)) // [1, 2, 3, 4]

知道了效果是什么样的了,我们可以去尝试着写这个 flatten 函数了

递归

我们最一开始能想到的莫过于循环数组元素,如果还是一个数组,就递归调用该方法:

// 方法 1
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        }
        else {
            result.push(arr[i])
        }
    }
    return result;
}


console.log(flatten(arr))

toString

如果数组的元素都是数字,那么我们可以考虑使用 toString 方法,因为:

[1, [2, [3, 4]]].toString() // "1,2,3,4"

调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗?

// 方法2
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.toString().split(',').map(function(item){
        return +item
    })
}

console.log(flatten(arr))

然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。

reduce

既然是对数组进行处理,最终返回一个值,我们就可以考虑使用 reduce 来简化代码:

// 方法3
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

console.log(flatten(arr))

...

ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:

var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]

我们用这种方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法:

// 方法4
var arr = [1, [2, [3, 4]]];

function flatten(arr) {

    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }

    return arr;
}

console.log(flatten(arr))

undercore

那么如何写一个抽象的扁平函数,来方便我们的开发呢,所有又到了我们抄袭 underscore 的时候了~

在这里直接给出源码和注释,但是要注意,这里的 flatten 函数并不是最终的 _.flatten,为了方便多个 API 进行调用,这里对扁平进行了更多的配置。

/**
 * 数组扁平化
 * @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;

}

解释下 strict,在代码里我们可以看出,当遍历数组元素时,如果元素不是数组,就会对 strict 取反的结果进行判断,如果设置 strict 为 true,就会跳过不进行任何处理,这意味着可以过滤非数组的元素,举个例子:

var arr = [1, 2, [3, 4]];
console.log(flatten(arr, true, true)); // [3, 4]

那么设置 strict 到底有什么用呢?不急,我们先看下 shallow 和 strct 各种值对应的结果:

  • shallow true + strict false :正常扁平一层
  • shallow false + strict false :正常扁平所有层
  • shallow true + strict true :去掉非数组元素
  • shallow false + strict true : 返回一个[]

我们看看 underscore 中哪些方法调用了 flatten 这个基本函数:

_.flatten

首先就是 _.flatten:

_.flatten = function(array, shallow) {
    return flatten(array, shallow, false);
};

在正常的扁平中,我们并不需要去掉非数组元素。

_.union

接下来是 _.union:

该函数传入多个数组,然后返回传入的数组的并集,

举个例子:

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

如果传入的参数并不是数组,就会将该参数跳过:

_.union([1, 2, 3], [101, 2, 1, 10], 4, 5);
=> [1, 2, 3, 101, 10]

为了实现这个效果,我们可以将传入的所有数组扁平化,然后去重,因为只能传入数组,这时候我们直接设置 strict 为 true,就可以跳过传入的非数组的元素。

// 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27)
function unique(array) {
   return Array.from(new Set(array));
}

_.union = function() {
    return unique(flatten(arguments, true, true));
}

_.difference

是不是感觉折腾 strict 有点用处了,我们再看一个 _.difference:

语法为:

_.difference(array, *others)

效果是取出来自 array 数组,并且不存在于多个 other 数组的元素。跟 _.union 一样,都会排除掉不是数组的元素。

举个例子:

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

实现方法也很简单,扁平 others 的数组,筛选出 array 中不在扁平化数组中的值:

function difference(array, ...rest) {

    rest = flatten(rest, true, true);

    return array.filter(function(item){
        return rest.indexOf(item) === -1;
    })
}

注意,以上实现的细节并不是完全按照 underscore,具体细节的实现感兴趣可以查看源码

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@FrontToEnd
Copy link

日常观光+学习

@kankk
Copy link

kankk commented Jul 26, 2017

学习了

@xiaobinwu
Copy link

学习了,之后应该可以用到

@sulingLiang
Copy link

image
为什么“如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。”,可以解释一下吗?因为我运行了,是没问题的

@HuangQiii
Copy link

@sulingLiang 扁平化不改变原数据啊,你的结果也显示了,string型的变成了数字都

@mqyqingfeng
Copy link
Owner Author

@HuangQiii 感谢回答~ (๑•̀ㅂ•́)و✧

@swpuLeo
Copy link

swpuLeo commented Apr 11, 2018

楼主你好,我发现你的 flatten() 有点问题:

先看 underscore 的:

const _ = require('underscore');
console.log(_.flatten([1, [2, [3, 4]], 5], false)); // [ 1, 2, 3, 4, 5 ]
console.log(_.flatten([1, [2, [3, 4]], 5], true)); // [ 1, 2, [ 3, 4 ], 5 ]

而楼主你的 flatten() 函数的表现如下:

/**
 * 数组扁平化
 * @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,
                    len = value.length;
                while (j < len) 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;

}

console.log(flatten([1, [2, [3, 4]], 5], false, false)); // [ 1, 2, 3, 4, 5 ]
console.log(flatten([1, [2, [3, 4]], 5], true, false)); // [ 1, 2, [ 3, 4 ] ] 这里与 underscore 不一致

Debug 发现,楼主的 len 发生了变化,原来是楼主的 flatten() 函数中声明了两次 len 变量,将第二个 len 换个名字就行了。

var j = 0,
    len = value.length;  // 改为 length = value.length

@mqyqingfeng
Copy link
Owner Author

@swpuLeo 感谢指出哈 (๑•̀ㅂ•́)و✧ 确实有问题,我自己测试了一下,除了改成 var length = value.length 之外,while (j < len) output[idx++] = value[j++]; 这里的 len 也需要改成 length

@thereisnowinter
Copy link

数组扁平化还可以这么写,不过初学者理解不了。

flatten = Function.apply.bind([].concat, [])

@Tan90Qian
Copy link

@mqyqingfeng 看到dalao用concat和扩展运算符的时候愣了一下,几乎完全这个方法的印象,甚至还是去查了文档才知道如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组。这一点的。。。dalao你除了面试、写这些博客时候,日常开发中哪里会用到concat么?我之前半年似乎这个方法连5次都没用到

@Tan90Qian
Copy link

@thereisnowinter dalao 666~萌新学到了新的一手 apply和bind连用,同时绑定调用的方法和调用的主体,只留下一个参数的位置

@mqyqingfeng
Copy link
Owner Author

mqyqingfeng commented Apr 24, 2018

@thereisnowinter 666~ 为你打 call~

稍微解释一下:

Function.apply.bind([].concat, [])

// 相当于

function(arg) {
  return Function.apply.call([].concat, [], arg)
}

// 相当于

function(arg) {
  return [].concat.apply([], arg)
}

// 相当于
// 这里错了
function(arg) {
   return [].concat(arg)
}

// 应该是

function(arg) {
   return [].concat(...arg)
}

@mqyqingfeng
Copy link
Owner Author

@Tan90Qian 处理数据或者需要返回一个新数组的时候,会用到 concat

@Tan90Qian
Copy link

@mqyqingfeng 那基本就是在不能使用es6的情况下使用咯?否则“扩展操作符”+数组直接量的创建方式基本可以替代它的功能。然后就是类似数组展平这样,仅靠一个concat或者扩展操作符无法完成的功能。

PS:刚写的内容有2个问题,

  1. 因为是用了bind,所以“相当于”下的代码应该都是return function而不是直接function

  2. 由于第三个函数是从第二个函数应用了apply之后转过去的,所以转化出来的结果应该是将arg参数拆开后再传入的结果,也就是借助了eval或者rest操作符的版本

return function(arg) {
  return [].concat(...arg)
}

这也和博主大大在正文中的版本一致。

@mqyqingfeng
Copy link
Owner Author

@Tan90Qian 关于这两个问题:

  1. 不需要 return function 吧?
var flatten = Function.apply.bind([].concat, [])

// 相当于

var flatten = function(arg) {
  return Function.apply.call([].concat, [], arg)
}

正好就是对应的,不需要 return function 呀

  1. 嗯……也不需要吧,arg 本身就会传入一个数组,concat 也支持直接传入一个数组,不需要展开呀

@Tan90Qian
Copy link

@mqyqingfeng 第一条是我弄错了。不过第二条,确实是需要展开的,因为MDN对concat方法的描述:如果要进行 concat() 操作的参数是数组,那么添加的是数组中的元素,而不是数组,因此

[].concat([1,[2,[3,4]]])  // [1,[2,[3,4]]]
[].concat(...[1,[2,[3,4]]])
// 相当于
[].concat(1,[2,[3,4]]) // [1,2,[3,4]]

@mqyqingfeng
Copy link
Owner Author

@Tan90Qian 啊,是的,感谢指出~

@Fiv5
Copy link

Fiv5 commented Apr 26, 2018 via email

@Fiv5
Copy link

Fiv5 commented Apr 26, 2018 via email

@Latube
Copy link

Latube commented Apr 26, 2018 via email

@mqyqingfeng
Copy link
Owner Author

@Fiv5 感谢告知~ 我并不知道这件事情,请教大家这种情况该怎么处理呢?

@delayk
Copy link

delayk commented Apr 27, 2018

@mqyqingfeng 微信公众号文章的话直接投诉就可以吧,有一个“未经授权的文章”选项

@huyong007
Copy link

/* 在javascript权威指南的第六版,p159*/
var objects=[{x:1,a:1},{y:2,a:2},{z:3,a:3}];
var leftunion=objects.reduce(union);//{x:1,y:2,z:3,a:1}
var rightunion=objects.reduceRight(union);//{x:1,y:2,z:3,a:3}
/书中p130页和p31页关于union和extend的设计是这样的/
function extend(o,p){
for(prop in p){
o[prop]=p[prop];
}
return o;
}

function union(o,p){return extend(extend({},o),p);}
/* 我的出来的结果和书中的出来的leftunion和rightunion的值正好是相反的,
书上也讲到,union()函数在碰到两个对象有同名属性时,使用第一个参数的属性值,
按照这个说法确实和结果相符,但是验证结果是他给出的union函数使用的extend()
使用第二个参数作为的属性值进行合并的,亲能帮我看看是我哪里错了吗?万分感谢,
我先去star一下您的项目啦 */

@huyong007
Copy link

楼主您的设计中第一个使用for循环的设计中我在实验这样一个 var arr = [1, [2, [[9, 0], 3, [2, 3]]]];结果给出的并不符合语气,我认为应该是您在设计for循环这个函数的时候外层的i被第二次出现的数组中的初始化给重置了,如果我们把for循环中的var改为let整个就没有任何问题了。

@xiaolb
Copy link

xiaolb commented Sep 1, 2018

楼主,有一个小疑问,就是方法1中将push变成concat为什么就得不到想要的结果了
// 方法 1
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
}
else {
result.concat(arr[i])
}
}
return result;
}

console.log(flatten(arr))

@zjp6049
Copy link

zjp6049 commented Feb 27, 2019

额 几个月前看没觉得什么 最近突然遇到扁平化才知道不用递归写出扁平化牛P了哈

@Chan-Chun
Copy link

最近ES10标准已经支持扁平化了!Array.flat(),以后也不用写这样的方法啦!
https://pawelgrzybek.com/whats-new-in-ecmascript-2019/#array-prototype-flat-array-prototype-flatmap-by-brian-terlson-michael-ficarra-and-mathias-bynens

@goSunadeod
Copy link

// 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; }
这个idx重置 有作用吗。。。麻烦又大佬知道的能告知一下,谢谢

@JSluowen
Copy link

JSluowen commented May 3, 2019

感觉又进步一点点了

@tr2v
Copy link

tr2v commented May 9, 2019

// 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; }
这个idx重置 有作用吗。。。麻烦又大佬知道的能告知一下,谢谢

递归数组后,output会变啊,不变idx,后面再插入数组项不就变成替换以后的了吗,我不明白的是这里怎么不直接用push呢

@goSunadeod
Copy link

goSunadeod commented May 9, 2019

// 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; }
这个idx重置 有作用吗。。。麻烦又大佬知道的能告知一下,谢谢

递归数组后,output会变啊,不变idx,后面再插入数组项不就变成替换以后的了吗,我不明白的是这里怎么不直接用push呢

递归时output会变,但不是函数里面会重新 定义var idx = output.length 吗 这和 递归函数之后的那句代码idx = output.length; 并没有关系啊

@tr2v
Copy link

tr2v commented May 10, 2019

递归时output会变,但不是函数里面会重新 定义var idx = output.length 吗 这和 递归函数之后的那句代码idx = output.length; 并没有关系啊

函数内的idx是最新的没错,但是外部的还是原来的,递归结束后肯定要再获取一下

@wbc0301
Copy link

wbc0301 commented Jun 24, 2019

console.log([].concat(...arr)) 这里用到的是函数的 rest 参数,大佬说成是数组的扩展运算符了

@NameWjp
Copy link

NameWjp commented Jul 10, 2019

关于 flatten = Function.apply.bind([].concat, [])的理解,想了半天总算理解了。。。
首先是bind函数改变了this指向,而apply函数内部正是通过this去拿的执行的函数(参考博主前面call和apply的实现,一开始不理解就在这。。),然后传递了了[]参数,故现在flatten函数实际执行的是[].concat,this指向是[],还接受一个数组(apply的第二个参数)。

@rongda
Copy link

rongda commented Jul 18, 2019

var arr1 = [1, 2, [3, 4]];
arr1.flat(Infinity)

@liuestc
Copy link

liuestc commented Nov 7, 2019

function flatten(input) {
  const stack = [...input];
  const res = [];
  while (stack.length) {
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop();
    if (Array.isArray(next)) {
      // 使用 push 送回内层数组中的元素,不会改动原始输入 original input
      stack.push(...next);
    } else {
      res.push(next);
    }
  }
  // 使用 reverse 恢复原数组的顺序
  return res.reverse();
}

leecode上看到的骚操作,感觉这个效率应该很高

@guanxc9876
Copy link

output[idx++] = value[j++];这种出来的数组下标是不规律的,为啥不直接用push

@Guard-233
Copy link

const flatten = (arr, shallow, strict) => {
  return arr.reduce((now, next) => {
    if (!shallow) {
      next = Array.isArray(next) ? flatten(next, shallow, strict) : next
    }
    if (strict && !Array.isArray(next)) {
      next = []
    }
    return now.concat(next)
  }, [])
}

稍微简化了一下

@bikedawuwang
Copy link

学习了

@buquan
Copy link

buquan commented Sep 23, 2020

扁平化

数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。

举个例子,假设有个名为 flatten 的函数可以做到数组扁平化,效果就会如下:

var arr = [1, [2, [3, 4]]];
console.log(flatten(arr)) // [1, 2, 3, 4]

知道了效果是什么样的了,我们可以去尝试着写这个 flatten 函数了

递归

我们最一开始能想到的莫过于循环数组元素,如果还是一个数组,就递归调用该方法:

// 方法 1
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        }
        else {
            result.push(arr[i])
        }
    }
    return result;
}


console.log(flatten(arr))

toString

如果数组的元素都是数字,那么我们可以考虑使用 toString 方法,因为:

[1, [2, [3, 4]]].toString() // "1,2,3,4"

调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗?

// 方法2
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.toString().split(',').map(function(item){
        return +item
    })
}

console.log(flatten(arr))

然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。

reduce

既然是对数组进行处理,最终返回一个值,我们就可以考虑使用 reduce 来简化代码:

// 方法3
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

console.log(flatten(arr))

...

ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:

var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]

我们用这种方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法:

// 方法4
var arr = [1, [2, [3, 4]]];

function flatten(arr) {

    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }

    return arr;
}

console.log(flatten(arr))

undercore

那么如何写一个抽象的扁平函数,来方便我们的开发呢,所有又到了我们抄袭 underscore 的时候了~

在这里直接给出源码和注释,但是要注意,这里的 flatten 函数并不是最终的 _.flatten,为了方便多个 API 进行调用,这里对扁平进行了更多的配置。

/**
 * 数组扁平化
 * @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;

}

解释下 strict,在代码里我们可以看出,当遍历数组元素时,如果元素不是数组,就会对 strict 取反的结果进行判断,如果设置 strict 为 true,就会跳过不进行任何处理,这意味着可以过滤非数组的元素,举个例子:

var arr = [1, 2, [3, 4]];
console.log(flatten(arr, true, true)); // [3, 4]

那么设置 strict 到底有什么用呢?不急,我们先看下 shallow 和 strct 各种值对应的结果:

  • shallow true + strict false :正常扁平一层
  • shallow false + strict false :正常扁平所有层
  • shallow true + strict true :去掉非数组元素
  • shallow false + strict true : 返回一个[]

我们看看 underscore 中哪些方法调用了 flatten 这个基本函数:

_.flatten

首先就是 _.flatten:

_.flatten = function(array, shallow) {
    return flatten(array, shallow, false);
};

在正常的扁平中,我们并不需要去掉非数组元素。

_.union

接下来是 _.union:

该函数传入多个数组,然后返回传入的数组的并集,

举个例子:

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

如果传入的参数并不是数组,就会将该参数跳过:

_.union([1, 2, 3], [101, 2, 1, 10], 4, 5);
=> [1, 2, 3, 101, 10]

为了实现这个效果,我们可以将传入的所有数组扁平化,然后去重,因为只能传入数组,这时候我们直接设置 strict 为 true,就可以跳过传入的非数组的元素。

// 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27)
function unique(array) {
   return Array.from(new Set(array));
}

_.union = function() {
    return unique(flatten(arguments, true, true));
}

_.difference

是不是感觉折腾 strict 有点用处了,我们再看一个 _.difference:

语法为:

_.difference(array, *others)

效果是取出来自 array 数组,并且不存在于多个 other 数组的元素。跟 _.union 一样,都会排除掉不是数组的元素。

举个例子:

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

实现方法也很简单,扁平 others 的数组,筛选出 array 中不在扁平化数组中的值:

function difference(array, ...rest) {

    rest = flatten(rest, true, true);

    return array.filter(function(item){
        return rest.indexOf(item) === -1;
    })
}

注意,以上实现的细节并不是完全按照 underscore,具体细节的实现感兴趣可以查看源码

专题系列

JavaScript专题系列目录地址:https://github.com/mqyqingfeng/Blog。

JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

学习了

+1

@Junezm
Copy link

Junezm commented Oct 10, 2020

解构运算原理来打平数组

function* flatten(arr) {
    for (const item of arr) {
        if(Array.isArray(item)){
            yield* flatten(item)
        } else {
            yield item
        }
    }
}

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

@anjina
Copy link

anjina commented Dec 13, 2020

可以修改原数组的扁平化

// splice 修改原数组
function flattern_v3(arr, depth) {
  if(depth < 1) return arr;

  let i, len;
  for(i=0, len=arr.length; i < len; i++) {
    const item = arr[i];

    if(Array.isArray(item) && depth) {
      arr.splice(i, 1, ...item);
      len += item.length - 1;
      depth--;
    }
  }
}

@bosens-China
Copy link

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

这个示例错误,正确的是

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

@banli17
Copy link

banli17 commented Apr 5, 2021

@bosens-China
[1,3],这个是在 arr 去掉 ...rest 里有的

@chloeeee72
Copy link

chloeeee72 commented Oct 18, 2021

可以直接使用数组的.flat()方法,在括号内传入提取嵌套数组的结构深度就行了,MDN:Array.prototype.flat()

@hotYan
Copy link

hotYan commented Apr 7, 2022

使用 Generator 函数实现数组扁平化:

function* flat(arr){
    if(Array.isArray(arr)){
        for(const item of arr){
            yield* flat(item)
        }
    }else{
       yield arr
    }
}
// 测试
let flatted = [ ...flat([1,2,[3,4,[5,6]]]) ]
console.log(flatted) // [1,2,3,4,5,6]

@RowlingJoe
Copy link

arr.join(',').split(',')

@rongda
Copy link

rongda commented Oct 11, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests