数组

46 阅读16分钟

一、数组基础

1. 数组概述

数组是我们最常用的数据类型之一,ECMAScript数组跟其他语言的数组一样,都是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。除此之外,ECMAScript数组的长度也是动态的,会随着数据的增删而改变。

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

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

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

let a = [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(); 
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(8.0); // [8]
Array(8.0); // [empty × 8]

Array.of(8.0, 5); // [8, 5]
Array(8.0, 5); // [8, 5]

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

2) Array.from

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

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

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

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

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

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

var obj = {0: 'a', 1: 'b', 2:'c', length: 3};

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

结果如图:

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

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

Array.from(obj, (value) => value.repeat(3));
//  控制台打印 (3) ["aaa", "bbb", "ccc"]

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

// String
Array.from('abc');                             // ["a", "b", "c"]
// Set
Array.from(new Set(['abc', 'def']));           // ["abc", "def"]
// Map
Array.from(new Map([[1, 'ab'], [2, 'de']]));   // [[1, 'ab'], [2, 'de']]

3. 数组空位

当我们使用数组字面量初始化数组时,可以使用一串逗号来创建空位,ECMAScript会将逗号之间相应索引位置的值当成空位,ES6 重新定义了该如何处理这些空位。

我们可以这样来创建一个空位数组:

let array = [,,,,,];
console.log(array.length);
console.log(array)

运行结果如下:

ES6新增的方法和迭代器与早期版本中存在的方法的行为不同,ES6新增方法普遍将这些空位当成存在的元素,只不过值为undefined,使用字面量形式创建如下数组:

let array = [1,,,5];
for(let i of array){
  console.log(i === undefined)
}
// 输出结果:false true true false

使用ES6的Array.form创建数组:

let array = Array.from([1,,,5]);
for(let i of array){
  console.log(i === undefined)
}
// 输出结果:false true true false

而ES6之前的方法则会忽略这个空位:

let array = [1,,,5];
console.log(array.map(() => 10))

// 输出结果:[10, undefined, undefined, 10]

由于不同方法对空位数组的处理方式不同,因此尽量避免使用空位数组。

4. 数组索引

在数组中,我们可以通过使用数组的索引来获取数组的值:

let colors = new Array("red", "blue", "green");  
console.log(array[1])  // blue

如果指定的索引值小于数组的元素数,就会返回存储在相应位置的元素,也可以通过这种方式来设置一个数组元素的值。如果设置的索引值大于数组的长度,那么就会将数组长度扩充至该索引值加一。

数组长度length的独特之处在于,他不是只读的。通过length属性,可以在数组末尾增加删除元素:

let colors = new Array("red", "blue", "green");  
colors.length = 2
console.log(colors[2])  // undefined

colors.length = 4
console.log(colors[3])  // undefined

数组长度始终比数组最后一个值的索引大1,这是因为索引值都是从0开始的。

5. 数组判断

一个很经典的ECMASript问题就是如何判断一个对象是不是数组,下面来看常用的数据类型检测的方法。

在 ES6 之前,至少有如下 5 种方式去判断一个对象是否为数组。

  • 通过Object.prototype.toString.call() 做判断:
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过constructor做判断:
obj.constructor === Array;
  • 通过instanceof做判断:
obj instanceof Array
  • 通过Array.prototype.isPrototypeOf做判断:
Array.prototype.isPrototypeOf(obj)
  • 通过基于getPrototypeOf做判断:
Object.getPrototypeOf(obj) === Array.prototype;

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

ES6 新增了 Array.isArray 方法,可以直接判断数据类型是否为数组:

Array.isArrray(obj);

如果 isArray 不存在,那么 Array.isArray 的 polyfill 通常可以这样写:

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

数组方法

改变原数组

1、增删改

1 栈方法

push 尾部添加,返回现数组长度

image.png

pop 尾部移除,返回移除元素

image.png

2 队列方法

shift 头部删除,返回删除元素

image.png

unshift 尾部添加,返回数组长度

image.png

3 任意位置增删改

splice (index, count, value1, value2,,,,,)

在数组index索引位置,删除count个元素,增加第三个参数开始的后续值;
返回删除掉的数组项,未删除返回[]

3.1 增

image.png

3.2 删

image.png

3.3 改

image.png

2、排序

sort 和 reverse

1 sort

sort(function(value1, value2){})

注意: 数组的sort方法不接受任何参数或者接受一个函数参数;

1.1 不接受任何参数,一般用于数组每项都是基本类型,那么排序结果按照unicode编码顺序从低到高,如:

image.png

1.2接收一个函数参数,函数参数为value1和value2,且value1-value2>1代表从小到大(大的往后排)

image.png

image.png

2 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

填充方法

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

array.fill(value, start, end)

其参数如下:

  • value ** **必需。填充的值;

  • start: 可选。开始填充位置;

  • end: 可选。停止填充位置 (默认为 array.length)。

使用示例如下:

const arr = [0, 0, 0, 0, 0];

// 用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);      // 重置

不改变原数组

  • 不改变原数组的方法:concat()、every()、filter()、find()、findIndex()、forEach()、indexOf()、join()、lastIndexOf()、map()、reduce()、reduceRight()、slice()、some。

搜索和位置方法

indexOf lastInexOf 返回索引

找item 的索引,存在返回索引,不存在返回-1;

lastIndexOf从后往前找,二者都是找到第一个值返回

image.png

include 返回布尔

image.png

find findIndex

二者都没后函数参数,find返回第一个元素,findIndex返回第一个元素的索引

image.png

操作方法

concat

数组拼接,常用在数组扁平化,只能扁平化一层

image.png

slice

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

arrayObject.slice(start,end)

其参数如下:

  • start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推;

  • end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。

使用示例如下:

let array = ["one", "two", "three", "four", "five"];
console.log(array.slice(0));    // ["one", "two", "three","four", "five"]
console.log(array.slice(2,3)); // ["three"]

转换方法

join 和 toString 用于转字符串; valueOf获取本身 image.png

归并方法

1 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((prev, cur, index, arr) => {
    console.log(prev, cur, index);
    return prev + 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((prev, cur, index, arr) => {
    console.log(prev, cur, index);
    return prev + cur;
}, 5)
console.log(arr, sum);  

输出结果如下:

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

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

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

2 reduceRight()

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

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

输出结果如下:

5 4 3
9 3 2
12 2 1
14 1 0
[1, 2, 3, 4] 15

循环遍历方法

map forEach some every filter

image.png

ECMAScript为数组定义了5个迭代方法,分别是every()、filter()、forEach()、map()、some()。这些方法都不会改变原数组。这五个方法都接收两个参数:以每一项为参数运行的函数和可选的作为函数运行上下文的作用域对象(影响函数中的this值)。传给每个方法的函数接收三个参数,分别是当前元素、当前元素的索引值、当前元素所属的数对象。

(1)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)
(2)map()

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

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

image.png

第二个参数用来绑定参数函数内部的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()方法:不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值;
(3)filter()

image.png 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]
(4)every()

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

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

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

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

其他方法

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

(1)for…in

image.png

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

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

其中两个参数:

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

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

使用示例如下:

const arr = [1, 2, 3]; 
 
for (var i in arr) { 
    console.log('键名:', i); 
    console.log('键值:', arr[i]); 
}

输出结果如下:

键名: 0
键值: 1
键名: 1
键值: 2
键名: 2
键值: 3

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

(2)for...of

image.png

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()一个数值参数,这个参数表示要打平的层级数:

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