[JavaScript编码能力]数组扁平化和数组去重

213 阅读6分钟

数组扁平化

Array.prototype.flat()(ES6)

按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回,对原数据没有影响。

  • depth 可选 指定要提取嵌套数组的结构深度,默认值为 1。
let arr1 = [1, 2, [3, 4]]
console.log(arr1.flat()) // [1, 2, 3, 4]

//默认深度为1
let arr2 = [1, 2, [3, 4, [5, 6]]]
console.log(arr2.flat()) // [1, 2, 3, 4, [5, 6]]

//指定展开 2 层深度的嵌套数组
let arr3 = [1, 2, [3, 4, [5, 6]]]
console.log(arr3.flat(2)) // [1, 2, 3, 4, 5, 6]

//使用 Infinity,可展开任意深度的嵌套数组
let arr4 = [1, 2, [3, 4, [5, 6, [7, 8, [9, 10]]]]]
console.log(arr4.flat(Infinity)) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

//扁平化跳过数组空项
let arr5 = [1, 2, , 3, 4]
console.log(arr5.flat()) // [1, 2, 3, 4]

//扁平化支持字符串和对象
let arr6 = [1, 2, ['hello world', [{ type: 'js' }, ['JavaScript']]]]
console.log(arr6.flat(Infinity)) // [1, 2, "hello world", {type: "js"}, "JavaScript"]

数组扁平化函数flat()实现方案

实现思路

首先遍历获取数组每个元素,然后判断该元素类型是否为数组,最后将数组类型的元素展开一层。同时递归遍历获取该数组的每个元素进行拉平处理。

遍历数组方案

const arr = [1, 1, 1, [2, 2, 2, [3, 3, 3, ['string', [{ type: 'object' }]]]]]
// for
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}
// for...of
for (let value of arr) {
    console.log(value)
}
// for...in
for (let i in arr) {
    console.log(arr[i])
}
// forEach
arr.forEach(value => {
    console.log(value)
})
// entries()
for (let [index, value] of arr.entries) {
    console.log(value)
}
// keys()
for (let value of arr.values()) {
    console.log(value)
}
// reduce()
arr.reduce((pre, cur) => {
    console.log(cur)
}, [])
// map()
arr.map(value => console.log(value))

判断数组元素是否为数组

const arr = [1, 1, 1, [2, 2, 2, [3, 3, 3, ['string', [{ type: 'object' }]]]]]
// true 构造函数 Array 的 prototype 属性是否出现在实例对象(arr)的原型链上
console.log(arr instanceof Array)
// true 数组实例(arr)继承了Array.prototype.constructor 属性,它的值就是 Array
console.log(arr.constructor === Array)
// true toString() 返回一个字符串,表示指定的数组(arr)及其元素
console.log(Object.prototype.toString.call(arr) === '[object Array]')
// true Array.isArray() 用于确定传递的值(arr)是否是一个 Array
console.log(Array.isArray(arr))

注意:

  1. instanceof操作符是假定只有一种全局环境。在浏览器中,我们的脚本可能需要在多个窗口之间交互。多个窗口意味着多个全局环境,不同的全局环境拥有不同的全局对象,从而拥有不同的内置类型构造函数。这一种情况下使用instanceof会不准确。
  2. constructor可以被重写,不确保一定是数组。

数组元素展开一层方案

const arr = [1, 1, 1, [2, 2, 2, [3, 3, 3, ['string', [{ type: 'object' }]]]]]
// 扩展运算符 + concat
console.log([].concat(...arr)) // [1, 1, 1, 2, 2, 2, [3, 3, 3, ['string', [{ type: 'object' }]]]]
// concat + apply
console.log([].concat.apply([], arr)) // [1, 1, 1, 2, 2, 2, [3, 3, 3, ['string', [{ type: 'object' }]]]]
// toString + split
const arr2 = [1, 1, 1, [2, 2, 2, [3, 3, 3, [4, 4, 4, [5, 5, 5]]]]]
console.log(arr.toString().split(',').map(value => parseInt(value))) // [1, 1, 1, 2, 2, 2, 3, 3, 3, NaN, NaN]
console.log(arr2.toString().split(',').map(value => parseInt(value))) // [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5]

注意:

toString + split 方式只有数组中元素是数字时可行。不推荐使用

for / for in / for of + 递归实现 flat()

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]
//for
//使用 void 0 去除空位
const flat = (arr) => {
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (Array.isArray(arr[i])) {
            res.push(...flat(arr[i]))
        } else if (arr[i] !== void 0) {
            res.push(arr[i])
        }
    }
    return res
}

console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]


//for in
function flat(arr) {
    let res = []
    for (let i in arr) {
        if (Array.isArray(arr[i])) {
            res.push(...flat(arr[i]))
        } else {
            res.push(arr[i])
        }
    }
    return res
}

console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

//for of 
const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]
//循环不能去除数组空位,需要手动清除 
function flat(arr) {
    let res = []
    for (let element of arr) {
        if (Array.isArray(element)) {
            res.push(...flat(element))
        } else {
            //去除空元素,添加非 undefined 元素(空元素在数字里会表现为 undefined )
            element !== void 0 && res.push(element)
        }
    }
    return res
}

console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

map / forEach + 递归实现 flat()

//map
const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]

function flat(arr) {
    let res = []
    arr.map(item => {
        if (Array.isArray(item)) {
            res.push(...flat(item))
        } else {
            res.push(item)
        }
    })
    return res
}

console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

//forEach
function flat(arr) {
    let res = []
    arr.forEach((element) => {
        if (Array.isArray(element)) {
            res.push(...flat(element))
        } else {
            res.push(element)
        }
    })
    return res
}

console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

reduce + 递归实现 flat()

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]

function flat(arr) {
    return arr.reduce((prev, cur) => {
        return prev.concat(Array.isArray(cur) ? flat(cur) : cur)
    }, [])
}
console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

用 Generator 实现 flat()

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]

function* flat(arr) {
    for (const item of arr) {
        if (Array.isArray(item)) {
            yield* flat(item)
        } else if (item !== void 0) {
            yield item
        }
    }
}
//调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
//也就是遍历器对象(Iterator Object)。所以我们要用一次扩展运算符得到结果。
var flattened = [...flat(arr)]
console.log(flattened) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

使用堆栈实现 flat()

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]
//使用 void 0 去除空位
function flat(arr) {
    //将数组展开一层并保存
    const stack = [...arr]
    const res = []
    //如果栈不为空
    while (stack.length) {
        //使用 pop 从 stack 中取出并移除值
        const next = stack.pop()
        if (Array.isArray(next)) {
            //使用 push 送回内层数组中的元素,不会改变原始输入
            stack.push(...next)
        } else if (next !== void 0) {
            res.push(next)
        }
    }
    //反转恢复数组的顺序
    return res.reverse()
}
console.log(flat(arr)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

使用 reduce + 递归实现通过参数打平数组

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]

function flat(arr, deep = 1) {
    //如果拉平深度大于0 则 reduce + 递归输出结果,否则浅拷贝当前数组
    return deep > 0 ? arr.reduce((prev, cur) => prev.concat(Array.isArray(cur) ? flat(cur, deep - 1) : cur), []) : arr.slice()
}

console.log(flat(arr, 5)) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

使用正则打平数组

const arr = [1, 1, 1, , [2, 2, 2, , [3, 3, 3, ['string', [{ type: 'object' }]]]]]
//利用JSON可以深拷贝数组的原理,替换掉全局'[' + ']',在结果两边拼接上'[' + ']',然后过滤掉 undefined null 
const res = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']').filter(d => d)
console.log(res) // [1, 1, 1, 2, 2, 2, 3, 3, 3, "string", {type: "object"}]

数组去重

使用 Set() 去重(ES6)

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
	//	通过 new Set 转换成 Set 数据结构的数据
	//	然后通过 Array.from 方法将 Set 数据结构的数据转换成正常数组
	//	返回的时候就是已去重的数据
    return Array.from(new Set(arr))
}

console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

//扩展运算符 + Set()
function unique(arr) {
    return [...new Set(arr)]
}
console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

优点:代码最少

缺点:{}不去重

使用双层循环+ splice() 去重(ES5常用)

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    var len = arr.length
    for (var i = 0; i < len; i++) {
        for (var j = i + 1; j < len; j++) {
            // 检查是否有重复的元素
            if (arr[i] === arr[j]) {
                // 有就从数组里删去重复的元素
                arr.splice(j, 1)
                //splice方法是在原数组基础上修改,此时删去一个元素,则长度减1
                len--
                //保证 j 的值经过 j++ 后不变
                j--
            }
        }
    }
    return arr
}

console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}NaN不去重

使用双层循环+ push() 去重(ES5常用)

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    var res = []
    var len = arr.length
    //遍历数组中所有元素
    for (var i = 0; i < len; i++) {
        for (var j = i + 1; j < len; j++) {
            // 检查是否有重复的元素,如果有终止当前循环同时进入顶层循环的下一轮判断
            if (arr[i] === arr[j]) {
                j = ++i
            }
        }
        //获取没有重复的值放入新的数组
        res.push(arr[i])
    }
    return res
}

console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}NaN不去重

使用 includes() + 遍历原数组去重

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var res = []
    for (var i = 0; i < arr.length; i++) {
        //如果新数组res不存在当前值,则将当前值推入res中
        if (!res.includes(arr[i])) {
            res.push(arr[i])
        }
    }
    return res
}

console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}不去重

使用 indexOf() + 遍历原数组去重

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var res = []
    var len = arr.length
    for (var i = 0; i < len; i++) {
        //如果新数组res不存在当前数组下标的元素,则将元素推入res中
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}

console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}NaN不去重

使用 sort() + 相邻元素比较去重

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    //先排序
    arr = arr.sort()
    var len = arr.length
    //取出第一位元素
    var res = [].concat(arr[0])
    //从第二位元素开始遍历
    for (var i = 1; i < len; i++) {
        //如果原数组相邻的两个元素值不相等
        if (arr[i] !== arr[i - 1]) {
            //将元素推入新数组 res
            res.push(arr[i])
        }
    }
    return res
}

console.log(unique(arr)) // [0, 1, 15, NaN, NaN, "NaN", {…}, {…}, "a", false, null, "true", true, undefined]

缺点:{}NaN不去重

使用 filter() + includes() 去重

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    var res = []
    return arr.filter(function (item, index, arr) {
        //使用过滤器,如果res包含值,返回空字符串,否则将元素值推入新数组res
        return res.includes(item) ? '' : res.push(item)
    })
}
console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}不去重

使用 filter() + hasOwnProperty() 去重(ES5,支持对象去重)

var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    var res = {}
    return arr.filter(function (item, index, arr) {
        return res.hasOwnProperty(typeof item + item) ? false : (res[typeof item + item] = true)
    })
}
console.log(unique(arr)) // [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]

优点:支持所有数据类型去重

这种方法是利用空对象,把数组的值保存成 Object 的 key 值,比如 Object[value] = true,在判断另一个值时,如果 Object[value] 存在就说明该值是重复的。

没有直接使用 obj[item] 是因为 1 和 "1" 是不同的,直接使用前面的方法会判断为同一个值。

因为对象的键值只能是字符串,所以我们可以使用typeof item + item拼成字符串作为key 来避免这个问题。

使用递归去重

var arr = [1, 1, '1', '1', 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    var res = arr
    var len = res.length
    res.sort(function (a, b) {
        return a - b //先排序,方便去重
    })
    function loop(index) {
        if (index >= 1) {
            if (res[index] === res[index - 1]) {
                res.splice(index, 1)
            }
            loop(index - 1)
        }
    }
    loop(len - 1)// 递归loop,然后数组去重
    return res
}
console.log(unique(arr)) // [1, "1", "true", false, null, true, 15, NaN, NaN, "NaN", 0, "a", {…}, {…}, undefined]

缺点:{}NaN不去重

使用 Map 数据结构去重

var arr = [1, 1, '1', '1', 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    let map = new Map()
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (map.has(arr[i])) {
            map.set(arr[i], true)
        } else {
            map.set(arr[i], false)
            res.push(arr[i])
        }
    }
    return res
}
console.log(unique(arr)) // [1, "1", "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}不去重

使用 reduce() + includes() 去重

var arr = [1, 1, '1', '1', 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 'NaN', 0, 0, 'a', 'a', {}, {}]

function unique(arr) {
    //reduce 累加器 如果包含元素就返回累加器,否则返回累加器和 cur 值的数组,thisArgs 是空数组,也就是累加器默认也是空数组
    return arr.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], [])
}
console.log(unique(arr)) // [1, "1", "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

缺点:{}不去重

参考:

Array.prototype.flat() MDN

一文搞定数组扁平化(超全面的数组拉平方案及实现)

32个手写JS,巩固你的JS基础(面试高频)

JavaScript数组去重(12种方法,史上最全)

数组去重的js实现方式