数组

145 阅读24分钟

一、数组基础

1. 数组概述

数组用于在单一变量中存储多个值,数组中每个槽位可以存储任意类型的数据。除此之外,ECMAScript数组的长度也是动态的,会随着数据的增删而改变。

数组是被等分为许多小块的连续内存段,每个小块都和一个整数关联,可以通过这个整数快速访问对应的小块。除此之外,数组拥有一个length属性,该属性表示的并不是数组元素的数量,而是指数组元素的最高序号加1。

let arr = [1, 2, 3];
arr.length === 3  // true

在ES6中,可以使用扩展运算符(...)来获取数组元素:

let arr = [1, 2, 3];
let b = [0, ...a, 4];  // [0, 1, 2, 3, 4]

2. 数组创建

数组的创建方式有以下两种。

(1)字面量

最常用的创建数组的方式就是数组字面量, 数组元素的类型可以是任意的,如下:

let colors = ["red", [1, 2, 3], true];  

(2)构造函数

使用构造函数创建数组的形式如下:

let array = new Array(); 

如果已知数组元素数量,那么就可以给构造函数传入一个数值,然后length属性就会被自动创建并保存这个值,比如创建一个长度为10的数组:

let array = new Array();  // [undefined × 10]

这样,就可以创建一个长度为10的数组,数组每个元素的值都是undefined。

还可以给Array构造函数传入要保存的元素,比如:

let colors = new Array("red", "blue", "green");  

这就出现问题了,当我们创建数组时,如果给数组传入一个值,如果传入的值是数字,那么就会创建一个长度为指定数字的数组;如果这个值是其他类型,就会创建一个质保函该特定制度额数组。这样我们就无法直接创建一个只包含一个数字的数组了。

Array 构造函数根据参数长度的不同,有如下两种不同的处理方式:

  • new Array(arg1, arg2,…) :参数长度为 0 或长度大于等于 2 时,传入的参数将按照顺序依次成为新数组的第 0 至第 N 项(参数长度为 0 时,返回空数组);

  • new Array(length) :当 length 不是数值时,返回一个只包含 length 元素一项的数组;当 length 为数值时,length 最大不能超过 32 位无符号整型,即需要小于 232,否则将抛出 RangeError。

在使用Array构造函数时,也可以省略 new 操作符,结果是一样的:

let array = Array();  

(3)ES6 构造器

鉴于数组的常用性,ES6 专门扩展了数组构造器 Array ,新增了 2 个方法:Array.of和Array.from。Array.of 用得比较少,Array.from 具有很强的灵活性。


1) Array.of

Array.of 用于将参数依次转化为数组项,然后返回这个新数组。它基本上与 Array 构造器功能一致,唯一的区别就在单个数字参数的处理上。

比如,在下面的代码中,可以看到:当参数为2个时,返回的结果是一致的;当参数是一个时,Array.of 会把参数变成数组里的一项,而构造器则会生成长度和第一个参数相同的空数组

Array.of(6.0); // [6]
Array(6.0); // [empty × 6]

Array.of(6.0, 5); // [6, 5]
Array(6.0, 5); // [6, 5]

Array.of('6'); // ["6"]
Array('6'); // ["6"]

array = new Array(6); // [空属性 × 6]

2) Array.from

Array.from 的设计初衷是快速基于其他对象创建新数组,准确来说就是从一个类似数组的可迭代对象中创建一个新的数组实例。其实,只要一个对象有迭代器,Array.from 就能把它变成一个数组(注意:该方法会返回一个的数组,不会改变原对象)。

从语法上看,Array.from 有 3 个参数:

  • 类似数组的对象,必选;

  • 加工函数,新生成的数组会经过该函数的加工再返回;

  • this 作用域,表示加工函数执行时 this 的值。

这三个参数里面第一个参数是必选的,后两个参数都是可选的:

var obj = {0: 'k', 1: 'o', 2:'b', 3:'e', length: 4};

Array.from(obj, function(value, index){
  console.log(value, index, this, arguments.length);
  return value.repeat(5);   //必须指定返回值,否则返回 undefined
}, obj);

// 输出结果为: ['kkkkk', 'ooooo', 'bbbbb', 'eeeee']

以上结果表明,通过 Array.from 这个方法可以自定义加工函数的处理方式,从而返回想要得到的值;如果不确定返回值,则会返回 undefined,最终生成的是一个包含若干个 undefined 元素的空数组。

实际上,如果这里不指定 this,加工函数就可以是一个箭头函数。上述代码可以简写为以下形式。

Array.from(obj, (value) => value.repeat(5));
//  输出结果为: ['kkkkk', 'ooooo', 'bbbbb', 'eeeee']

除了上述 obj 对象以外,拥有迭代器的对象还包括 String、Set、Map 等,Array.from 都可以进行处理:

// String
Array.from('kobe');                             // ['k', 'o', 'b', 'e']
// Set
Array.from(new Set(['kobe', 'bryant']));           // ['kobe', 'bryant']
// Map
Array.from(new Map([[8, 'kobe'], [24, 'bryant']]));   // [[8, 'kobe'], [24, 'bryant']]

3.数组判断

这是一道非常经典的题目,方法如下:

  • 通过Object.prototype.toString.call() 做判断:(最常用的方法)
Object.prototype.toString.call(arr).slice(8,-1) === 'Array';
  • 通过constructor做判断:
arr.constructor === Array;
  • 通过instanceof做判断:
arr instanceof Array
  • 通过Array.prototype.isPrototypeOf做判断:
Array.prototype.isPrototypeOf(arr)
  • 通过基于getPrototypeOf做判断:
Object.getPrototypeOf(arr) === Array.prototype 

如果obj是一个数组,那么上面这 5 个判断全部为 true,推荐通过 Object.prototype.toString 去判断一个值的类型。

ES6 新增了 Array.isArray 方法,可以直接判断数据类型是否为数组:(非常简便好用)

Array.isArrray(arr);

如果 isArray 不存在,那么 Array.isArray 的 polyfill 通常可以这样写:(其实也就是活用了第一种方法)

if (!Array.isArray){
  Array.isArray = function(arg){
    return Object.prototype.toString.call(arg) === '[object Array]';
  };
}

二、数组方法

  • 改变原数组的方法:push()、pop()、shift()、unshift()、reverse()、sort()、splice()、fill();(前面七个是vue2.0监听的改变数组的7个方法,后面的fill不太清楚)
  • 不改变原数组的方法:除了上述以外的其他所有方法

改变数组的方法

增加的方法返回的都是数组长度,删除的方法返回的都是删除的值

(1)push()

push()方法可以接收任意数量的参数,并将它们添加了数组末尾,并返回数组新的长度。该方法会改变原数组。

使用示例如下:

let word = ["mamba", "never"];
let length = word.push("out");
console.log(word); // ['mamba', 'never', 'out']
console.log(length); // 3

(2)pop()

pop() 方法用于删除并返回数组的最后一个元素。它没有参数。该方法会改变原数组。 其语法形式如下:

使用示例如下:

let word = ["mamba", "never", "out"];
let item = word.pop();
console.log(word); // ['mamba', 'never']
console.log(item); // out

(3)shift()

shift()方法会删除数组的第一项,并返回它,然后数组长度减一,该方法会改变原数组。

使用示例如下:

let word = ["mamba", "never", "out"];
let item = word.shift();
console.log(word); // ["never", "out"]
console.log(item); // mamba

注意:如果数组是空的,那么 shift() 方法将不进行任何操作,返回 undefined 值。

(4)unshift()

unshift()方法可向数组的开头添加一个或更多元素,并返回新的长度。该方法会改变原数组。 使用示例如下:

let word = ["mamba", "never", "out"];
let length = word.unshift('kobe');
console.log(word); // ['kobe', 'mamba', 'never', 'out']
console.log(length); // 4

(5)splice()

splice()方法可能是数组中的最强大的方法之一了,使用它的形式有很多种,它会向/从数组中添加/删除项目,返回被删除的项目。该方法会改变原始数组。其使用语法如下:

arr.splice(index, count, item1,.....,itemN)

其参数如下:

  • index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
  • count:必需。要删除的项目数量。如果设置为 0,则不会删除项目。
  • item1, ..., itemN:可选。向数组添加的新项目。

从上面参数可知,splice主要有三种使用形式:

  • 删除: 需要给splice()传递两个参数,即要删除的第一个元素的位置和要删除的元素的数量;

  • 插入: 需要给splice()传递至少三个参数,即开始位置、0(要删除的元素数量)、要插入的元素。

  • 替换: splice()方法可以在删除元素的同事在指定位置插入新的元素。同样需要传入至少三个参数,即开始位置、要删除的元素数量、要插入的元素。要插入的元素数量是任意的,不一定和删除的元素数量相等。

使用示例如下:

let word = ["mamba", "never", "out"];
console.log(word.splice(1, 2));            // 删除:["never", "out"]

let word = ["mamba", "never", "out"];
console.log(word.splice(0, 0, 'kobe', 'say'));      // 插入:[]
console.log(word) //  ['kobe', 'say', 'mamba', 'never', 'out']

let word = ['kobe', 'say', 'mamba', 'never', 'out']
console.log(word.splice(0, 2, "who", "think"));  // 替换:['kobe', 'say']
console.log(word)     //  ['who', 'think', 'mamba', 'never', 'out']

(6)sort()

sort()方法是我们常用给的数组排序方法,该方法会在原数组上进行排序,会改变原数组,其使用语法如下:

arrayObject.sort(sortby)

其中参数sortby是可选参数,用来规定排序顺序,它是一个比较函数,用来判断哪个值应该排在前面。默认情况下,sort()方法会按照升序重新排列数组元素。为此,sort()默认是按照字典序来排的,如下代码:

let array = [5, 4, 3, 2, 1];
let array2 = array.sort();
console.log(array2)  // [1, 2, 3, 4, 5]

let array = [0, 1, 5, 10, 15];
let array2 = array.sort();
console.log(array2)  //  [0, 1, 10, 15, 5]

可以看到,上面第二段代码就出现了问题,虽然5是小于10的,但是字符串10在5的前面,所以10还是会排在5前面,因此可知,在很多情况下,不添加参数是不行的。

对于sort()方法的参数,它是一个比较函数,它接收两个参数,如果第一个参数应该排在第二个参数前面,就返回-1;如果两个参数相等,就返回0;如果第一个参数应该排在第二个参数后面,就返回1。一个比较函数的形式可以如下:

function compare(value1, value2) {
	if(value1 < value2){
  	return -1
  } else if(value1 > value2){
  	return 1
  } else{
  	return 0
  }
}

let array = [0, 1, 5, 10, 15];
let array2 = array.sort(compare);
console.log(array2)  // [0, 1, 5, 10, 15]

我们使用箭头函数来定义:

let array = [0, 1, 5, 10, 15];

let array2 = array.sort((a, b) => a - b);  // 正序排序
console.log(array2)  // [0, 1, 5, 10, 15]

let array3 = array.sort((a, b) => b - a);  // 倒序排序
console.log(array3)  // [15, 10, 5, 1, 0]

(7)reverse()

reverse() 方法用于颠倒数组中元素的顺序。该方法会改变原来的数组,而不会创建新的数组。其使用语法如下:

arrayObject.reverse()

使用示例如下:

let array = [1,2,3,4,5];
let array2 = array.reverse();
console.log(array);   // [5,4,3,2,1]
console.log(array2 === array);   // true

(8)fill()

使用fill()方法可以向一个已有数组中插入全部或部分相同的值,开始索引用于指定开始填充的位置,它是可选的。如果不提供结束索引,则一直填充到数组末尾。如果是负值,则将从负值加上数组的长度而得到的值开始。该方法的语法如下:

array.fill(value, start, end)

其参数如下:

  • value ** **必需。填充的值;
  • start: 可选。开始填充位置;
  • end: 可选。停止填充位置 (默认为 array.length)。 使用示例如下:

// 用5填充整个数组
arr.fill(5);
console.log(arr); // [5, 5, 5, 5, 5]
arr.fill(0);      // 重置

// 用5填充索引大于等于3的元素
arr.fill(5, 3);
console.log(arr); // [0, 0, 0, 5, 5]
arr.fill(0);      // 重置

// 用5填充索引大于等于1且小于3的元素
arr.fill(5, 3);
console.log(arr); // [0, 5, 5, 0, 0]
arr.fill(0);      // 重置

// 用5填充索引大于等于-1的元素
arr.fill(5, -1);
console.log(arr); // [0, 0, 0, 0, 5]
arr.fill(0);      // 重置

不改变数组的方法

(1)concat()

concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。其适用语法如下:

arrayObject.concat(arrayX,arrayX,......,arrayX)

其中参数arrayX是必需的。该参数可以是具体的值,也可以是数组对象。可以是任意多个。

使用示例如下:

let array = [1, 2, 3];
let array2 = array.concat(4, [5, 6], [7, 8, 9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array);  // [1, 2, 3], 可见原数组并未被修改

该方法还可以用于数组扁平化,后面会介绍。

(2)toString()/toLocaleString()(数组转换成字符串的利器,可以用来数组拍平

toString()方法返回的是由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串,也就是说,对数组的每个值都会调用toString()方法,以得到最终的字符串:

let arr = ["mamba", "never", "out"]; 
console.log(arr.toString())  // mamba,never,out

(3)valueOf()

valueOf()方法返回的是数组本身,如下面代码:

let arr = ["mamba", "never", "out"];
console.log(arr.valueOf())  // ['mamba', 'never', 'out']

(4)join()

join() 方法用于把数组中的所有元素放入一个字符串。元素是通过指定的分隔符进行分隔的。其使用语法如下:

arrayObject.join(separator)

其中参数separator是可选的,用来指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符。

该方法返回一个字符串。该字符串是通过把 arrayObject 的每个元素转换为字符串,然后把这些字符串连接起来,在两个元素之间插入 separator 字符串而生成的。

使用示例如下:

let arr = ["mamba", "never", "out"];
console.log(arr.join());      // mamba,never,out
console.log(arr.join("-"));   // mamba-never-out

(5)reduce()

reduce() 方法对数组中的每个元素执行一个reducer函数(升序执行),将其结果汇总为单个返回值。其使用语法如下:

arr.reduce(callback,[initialValue])

reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,接受四个参数:初始值(或者上一次回调函数的返回值),当前元素值,当前索引,调用 reduce 的数组。

(1) callback (执行数组中每个值的函数,包含四个参数)

  • previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
  • currentValue (数组中当前被处理的元素)
  • index (当前元素在数组中的索引)
  • array (调用 reduce 的数组)

(2) initialValue (作为第一次调用 callback 的第一个参数。)

let arr = [1, 2, 3, 4]
let sum = arr.reduce((pre, cur, index, arr) => {
    console.log(pre, cur, index);
    return pre + cur;
})
console.log(arr, sum);  

输出结果如下:

1 2 1
3 3 2
6 4 3
[1, 2, 3, 4] 10

再来加一个初始值看看:

let arr = [1, 2, 3, 4]
let sum = arr.reduce((pre, cur, index, arr) => {
    console.log(pre, cur, index);
    return pre + cur;
}, 10)
console.log(arr, sum);  

输出结果如下:

5 1 0
6 2 1
8 3 2
11 4 3
[1, 2, 3, 4] 20

通过上面例子,可以得出结论:如果没有提供initialValue,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。

注意,该方法如果添加初始值,就会改变原数组,将这个初始值放在数组的最后一位。

(6)reduceRight()

该方法和的上面的reduce()用法几乎一致,只是该方法是对数组进行倒序查找的。而reduce()方法是正序执行的。

let arr = [1, 2, 3, 4]
let sum = arr.reduceRight((pre, cur, index, arr) => {
    console.log(pre, cur, index);
    return pre + cur;
}, 10)
console.log(arr, sum);

输出结果如下:

10 4 3
14 3 2
17 2 1
19 1 0
[1, 2, 3, 4] 20

(7)concat()

concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。其适用语法如下:

arrayObject.concat(arrayZ,arrayZ,......,arrayZ)

其中参数arrayZ是必需的。该参数可以是具体的值,也可以是数组对象。可以是任意多个。

使用示例如下:

let array = [1, 2, 3];
let array2 = array.concat(4, [5, 6], [7, 8, 9]);
console.log(array2); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(array);  // [1, 2, 3], 可见原数组并未被修改

该方法还可以用于数组扁平化,后面会介绍。

(8)slice()

slice() 方法可从已有的数组中返回选定的元素。返回一个新的数组,包含从 start 到 end (不包括该元素)的数组元素。方法并不会修改数组,而是返回一个子数组。其使用语法如下:

其参数如下:

  • start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推;
  • end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
  • 使用示例如下:
let word = ["mamba", "never", "out"];
console.log(word.slice(0));    // ["mamba", "never", "out"]
console.log(word.slice(1,2)); // ["never"]

(3)splice()

splice()方法可能是数组中的最强大的方法之一了,使用它的形式有很

(9)搜索方法 indexOf()、lastIndexOf()、includes()、find()、findIndex()

使用示例如下:

let arr = [1, 2, 3, 4, 5];
console.log(arr.indexOf(4))      // 3
console.log(arr.lastIndexOf(3))  // 2
console.log(arr.includes(5))     // true
let arr = [1, 2, 3, 4, 5]
arr.find(item => item > 2)      // 结果: 3 // 第一次出现的值
arr.findIndex(item => item > 2) // 结果: 2 // 第一次出现的坐标

迭代器方法 every()、filter()、forEach()、map()、some()

这五个方法都接收两个参数:以每一项为参数运行的函数和可选的作为函数运行上下文的作用域对象(影响函数中的this值)。传给每个方法的函数接收三个参数,分别是当前元素、当前元素的索引值、当前元素所属的数对象。

(10)forEach()

forEach 方法用于调用数组的每个元素,并将元素传递给回调函数。该方法没有返回值,使用示例如下:

let arr = [1,2,3,4,5]
arr.forEach((item, index, arr) => {
  console.log(index+":"+item)
})

该方法还可以有第二个参数,用来绑定回调函数内部this变量(回调函数不能是箭头函数,因为箭头函数没有this):

let arr = [1,2,3,4,5]
let arr1 = [9,8,7,6,5]
arr.forEach(function(item, index, arr){
  console.log(this[index])  //  9 8 7 6 5
}, arr1)

(11)map()

map() 方法会返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。该方法按照原始数组元素顺序依次处理元素。该方法不会对空数组进行检测,它会返回一个新数组,不会改变原始数组。使用示例如下:

let arr = [1, 2, 3];
 
arr.map(item => {
    return item+1;
})
// 结果: [2, 3, 4]

第二个参数用来绑定参数函数内部的this变量:

var arr = ['a', 'b', 'c'];
 
[1, 2].map(function (e) {
    return this[e];
}, arr)
 // 结果: ['b', 'c']

该方法可以进行链式调用:

let arr = [1, 2, 3];
 
arr.map(item => item+1).map(item => item+1)
 // 结果: [3, 4, 5]

forEach和map 区别如下:

  • forEach()方法:会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map()方法:不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;

(12)filter()

filter()方法用于过滤数组,满足条件的元素会被返回。它的参数是一个回调函数,所有数组元素依次执行该函数,返回结果为true的元素会被返回。该方法会返回一个新的数组,不会改变原数组。

let arr = [1, 2, 3, 4, 5]
arr.filter(item => item > 2) 
// 结果:[3, 4, 5]

可以使用filter()方法来移除数组中的undefined、null、NAN等值

let arr = [1, undefined, 2, null, 3, false, '', 4, 0]
arr.filter(Boolean)
// 结果:[1, 2, 3, 4]
console.log([1,0,, false, undefined, '', null, NaN].filter(Boolean))
// [1]

(13)every()

该方法会对数组中的每一项进行遍历,只有所有元素都符合条件时,才返回true,否则就返回false。

let arr = [1, 2, 3, 4, 5]
arr.every(item => item > 0) 
// 结果: true

(14)some()

该方法会对数组中的每一项进行遍历,只要有一个元素符合条件,就返回true,否则就返回false。

let arr = [1, 2, 3, 4, 5]
arr.some(item => item > 4) 
// 结果: true

其他方法

除了上述方法,遍历数组的方法还有for...in和for...of。下面就来简单看一下。

(1)for…in

for…in 主要用于对数组或者对象的属性进行循环操作。循环中的代码每执行一次,就会对对象的属性进行一次操作。其使用语法如下:

for (var item in object) {
  执行的代码块
}

其中两个参数:

  • item:必须。指定的变量可以是数组元素,也可以是对象的属性。

  • object:必须。指定迭代的的对象。

使用示例如下:

const arr = [4, 5, 6]; 
 
for (var i in arr) { 
    console.log('键名:', i); 
    console.log('键值:', arr[i]); 
}

输出结果如下:

键名: 0
键值: 4
键名: 1
键值: 5
键名: 2
键值: 6

需要注意,该方法不仅会遍历当前的对象所有的可枚举属性,还会遍历其原型链上的属性。 除此之外,该方法遍历数组时候,遍历出来的是数组的索引值,遍历对象的时候,遍历出来的是键值名。

(2)for...of

for...of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of 循环,以替代 for...inforEach() ,并支持新的迭代协议。for...of 允许遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合)等可迭代的数据结构等。

语法:

for (var item of iterable) {
    执行的代码块
}

其中两个参数:

  • item:每个迭代的属性值被分配给该变量。

  • iterable:一个具有可枚举属性并且可以迭代的对象。

该方法允许获取对象的键值:

var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
  console.log(a); // 0 1 2 3
}
for (let a of arr) {
  console.log(a); // a b c d
}

该方法只会遍历当前对象的属性,不会遍历其原型链上的属性。


注意:

  • for...of适用遍历 数组/ 类数组/字符串/map/set 等拥有迭代器对象的集合;
  • 它可以正确响应break、continue和return语句;
  • for...of循环不支持遍历普通对象,因为没有迭代器对象。如果想要遍历一个对象的属性,可以用for-in循环。

总结,for…of 和for…in的区别如下:

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

(3)flat()

在ES2019中,flat()方法用于创建并返回一个新数组,这个新数组包含与它调用flat()的数组相同的元素,只不过其中任何本身也是数组的元素会被打平填充到返回的数组中:

[1, [2, 3]].flat()   // [1, 2, 3]
[1, [2, [3, 4]]].flat()   // [1, 2, [3, 4]]

在不传参数时,flat()默认只会打平一级嵌套,如果想要打平更多的层级,就需要传给flat()一个数值参数,这个参数表示要打平的层级数,可以用Infinity作为参数进行拍平:

[1, [2, [3, 4]]].flat(2)   // [1, 2, 3, 4]
[1, [2, [3, 4]]].flat(Infinity)   // [1, 2, 3, 4]

三、数组常见操作

1. 数组扁平化

目标的输出效果如下:

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

简单来说就是把多维的数组“拍平”,输出最后的一维数组。下面来看看实现flatten函数的方式。

(1)递归实现

普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果某一项还是一个数组,那么就继续往下遍历,利用递归来实现数组的每一项的连接:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
    let result = []
    for (let i = 0; i<arr.length; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result
}
flatten(arr);  //  [1, 2, 3, 4,5]

可以看到,最后返回的结果是扁平化的结果,这段代码核心就是循环遍历过程中的递归操作,首先临界条件是非数组是push当前元素,如果是数组,那么递归得到结果并concat原数组结果,然后返回,如果是数组就concat递归后的值

(2)reduce 函数迭代

从上面的递归函数可以看出,其实就是对数组的每一项进行处理,那么其实也可以用 reduce 来实现数组的拼接(其实也是个递归),从而简化上面方法的代码,改造后的代码如下:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
    return arr.reduce((pre, cur, index, array) => {
       return pre.concat(Array.isArray(cur) ? flatten(cur): cur)
    }, [])
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]

reduce 的第一个参数用来返回最后累加的结果,思路和第一种递归方法是一样的,但是通过使用 reduce 之后代码变得更简洁了,也同样解决了扁平化的问题。

(3)扩展运算符实现

这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr)
    }
    return arr
}
console.log(flatten(arr));//  [1, 2, 3, 4,5]

从执行的结果中可以发现,先用数组的 some 方法把数组中仍然是组数的项过滤出来,然后执行 concat 操作,利用 ES6 的展开运算符,将其拼接到原数组中,最后返回原数组,达到了预期的效果。

(4)split 和 toString(这个方法很玄妙)

可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:

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

通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组。

(5)ES6 中的 flat

我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth]) 其中 depth是展开的层数,默认是1。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:

let arr = [1, [2, [3, 4, 5]]];
function flatten(arr) {
  return arr.flat(Infinity);
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5]

可以看出,一个嵌套了两层的数组,通过将 flat 方法的参数设置为 Infinity,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。在编程过程中,如果数组的嵌套层数不确定,最好直接使用 Infinity,可以达到扁平化的效果。

(6)正则和 JSON 方法

在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:

let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
  let str = JSON.stringify(arr) // '[1,[2,[3,[4,5]]],6]'
  const reg = /\[|\]/g
  str = str.replace(reg, '')
  str = `[${str}]`
  return JSON.parse(str)
}
console.log(flatten(arr)); //  [1, 2, 3, 4,5, 6]

可以看到,其中先把传入的数组转换成字符串,然后通过正则表达式的方式把括号过滤掉,匹配规则是:全局匹配(g)左括号或者右括号,将它们替换成空格,最后返回处理后的结果。之后拿着正则处理好的结果重新在外层包裹括号,最后通过 JSON.parse 转换成数组返回。

2. 数组去重

去除无序数组中的重复元素并且返回新的无重复数组。

(1)Set实现

ES6方法(使用数据结构集合):

const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];
Array.from(new Set(arr)); // [1, 2, 3, 4, 5]

(2)hash实现

使用hash存储不重复的数字 // 此处也可以不用hasOwnProperty

const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1];

function uniqueArray(arr) {
  let hash = {}
  let res = []
  for (let i = 0; i<arr.length; i++) {
      if (!hash[arr[i]])
      // if (!hash.hasOwnProperty([arr[i]])) {
          hash[arr[i]] = 1
          res.push(arr[i])
      }
  }
    return res
}
uniqueArray(arr) // [1, 2, 3, 4, 5]

3. 数组求和

(1)reduce实现

let arr = [1, 2, 3, 4, 5, 6]
let sum = arr.reduce((pre, cur, index, arr) => {
    return pre+cur
}, 0);
console.log(sum); // 21

(2)递归实现(感觉没什么必要)

let arr = [1, 2, 3, 4, 5, 6] 
function add(arr) {
    if (arr.length == 1) return arr[0] 
    return arr[0] + add(arr.slice(1)) 
}
console.log(add(arr))  // 21

4. 数组乱序

(1)正向遍历

主要的实现思路就是:

  1. 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换;
  2. 首先Math.round是上下取整,2.49=>2, 2.52=>3;Math.random()是[0,1)
  3. 我们要确保取出来的arr.length - 1 - i + i是 arr的最大索引值,然后前面随机,后面相加即可,也有可能是自己的索引,然后交换值即可
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (var i = 0; i < arr.length; i++) {
  const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
}
console.log(arr)