第06章 数组

108 阅读13分钟

一、概述

数组,即集合,所谓集合,就是存储数据的容器。我们可以把教室理解为一个集合,而学生作为存储在集合里面的数据,叫做元素。

JavaScript集合类型主要使用数组,数组为一个特殊对象。ES6新增了Set数据结构,也用于表示一个集合。下面将主要介绍数组相关的知识点。

数组是有序排列的一组值的集合,这些值被包含在一对方括号内,里面的这些值称作数组元素。每个数组内的值都会有一个键名,这些键名其实就是一个从0开始依次计 数的下标,每个数组都会有一个“length”属性,表示数组元素的个数,所以数组内元素最大的下标值为**“length - 1”**。

数组内的值可以是JavaScript允许的任何类型的值,甚至可以是数组自身,像这样数组内包含另外一个数组的数组被称作 二维数组 任然继续包含数组,这样的数组就会形成“多维数组”。数组的基本表现形式如下:

var arr = [1, "a", {}, [], false];

提示:数组虽然有很多独立的特性,但它的数据类型仍然为对象型(object)。

二、创建数组

// 1、字面量方法(优先使用)
var arr1 = [1, 2, 3];

// 2、对象构造方法
var arr2 = new Array(1, 2, 3);

// 3、数组也可以先声明后赋值
var arr3 = [];
arr3[0] = 'A';
arr3[3] = 'C';
// (4) ['A', empty × 2, 'C']

var arr4 = new Array();
arr4[0] = 'A';
arr4[3] = 'C';
// (4) ['A', empty × 2, 'C']

// 4、填充指定长度的数组
var arr5 = new Array(3).fill(0);
// (3) [0, 0, 0]

// 5. 对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例
var a1 = [1, 2];
var a2 = Array.from(a1);
a1
// (2) [1, 2]
a1 === a2 
// false

三、访问 & 修改数组元素 *

  • 访问:数组名[下标]
  • 修改:数组名[下标] = 新值
var names = ["达摩", "张良", "吕布"];

// 1. 访问数组元素
names[0]; 
names.at(0);
// "达摩"

// 2. 修改数组元素
names[1] = "貂蝉"; 
// (3)["达摩", "貂蝉", "吕布"]

四、数组空位

in 运算符用于检测数组元素的某个位置是否存在元素,返回的是一个布尔值。如果为 false,则表示对应下标位置的值不存,反之亦然。

var heros = ["达摩", , "貂蝉", "吕布"];

heros
//(4) ["达摩", empty, "貂蝉", "吕布"]

0 in heros;
true
1 in heros;
false
2 in heros;
true
heros[1];
undefined

需要在这里进行补充说明的是,上例中下标为1的地方没有值,称作 “空位(hole)”,它返回的值为 undefined,但仍然会被计入数组的长度属性 length 中。而length的值是由数组中最后一个元素的下标+1决定的,我们来看一个示例:

var heros = ["达摩", "貂蝉", "吕布"];
heros.length
3
heros[99] = "李白
heros.length
100

五、数组对象 *

1. 长度

通过 length 属性访问数组元素长度

var heros = ["达摩", "貂蝉", "吕布"];
heros.length
3

2. 数组判断

判断某个对象是否是一个数组使用 Array.isArray() 方法,如果是,则返回 true,否则返回 false

Array.isArray("");
false
Array.isArray({});
false
Array.isArray(true);
false
Array.isArray([]);
true

3. 添加元素

添加数组元素的方法比较多,主要有以下种:

  • 数组名[下标]:通过下标添加元素,效率高,建议使用。(改变原始数组)

  • push():在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度*( 改变原数组)*。

  • unshift():在数组的开始位置添加一个或多个元素,并返回添加新元素后的数组长度*( 改变原数组)*。

var heros = ["达摩", "貂蝉", "吕布"];
// 1. 数组名[下标],只能在末尾添加
heros[heros.length] = "露娜";
heros
(4) ["达摩", "貂蝉", "吕布", "露娜"]

// 2. push()
heros.push("上官婉儿", "嬴政");
heros
(6) ["达摩", "貂蝉", "吕布", "露娜", "上官婉儿", "嬴政"]

// 3. unshift()
heros.unshift("李白");
7
heros
(7) ["李白", "达摩", "貂蝉", "吕布", "露娜", "上官婉儿", "嬴政"]

4. 合并数组

  • apply():合并数组(只能合并两个)( 改变原数组)
  • concat():合并数组(允许合并多个数组) ( 不改变原数组)
  • [...[]]:ES6 序列化合并数组,高效简洁,建议使用。
var a = ["李白"];
var b = ["王昭君", "虞姬"];

// 1. apply
Array.prototype.push.apply(a, b);
a
(3) ["李白", "王昭君", "虞姬"]
b
(2) ["王昭君", "虞姬"]

// 2. concat
var arr = a.concat(b);
arr
(3) ["李白", "王昭君", "虞姬"]

// 3. [...[]]
var arr = [...a, ...b];
arr
(3) ["李白", "王昭君", "虞姬"]

5. 删除元素

删除数组元素的方法如下:

  • pop():删除数组最后一个元素*( 改变原数组)*。
  • shift():删除数组第一个元素*( 改变原数组)*。
  • splice():范围删除*( 改变原数组)*。
var heros = ["露娜", "上官婉儿", "李白", "嬴政", "貂蝉", "兰陵王", "亚瑟"];
// 1. pop
heros.pop();
heros
(6) ["露娜", "上官婉儿", "李白", "嬴政", "貂蝉", "兰陵王"]

// 2. shift 
heros.shift();
heros
(5) ["上官婉儿", "李白", "嬴政", "貂蝉", "兰陵王"]

提示:以上两种方法在删除数组元素时只能一个一个删除。

删除数组元素中 splice 方法最为灵活,不仅可以范围删除数组元素,还可以实现数组元素的插入和替换。该方法语法形式为:

splice(location, count, items...)

参数解读:

  • location:删除的起始位置
  • count:删除的元素个数
  • items:要被插入数组的新元素。
// 1. 删除 -> 删除李白、嬴政
heros.splice(1, 2);
heros
(3) ["上官婉儿", "貂蝉", "兰陵王"]

// 2. 替换 -> 把兰陵王替换成鲁班七号
heros.splice(2, 1, "鲁班七号");
heros
(3) ["上官婉儿", "貂蝉", "鲁班七号"]

// 3. 插入 -> 在鲁班七号之前插入吕布、赵云
heros.splice(2, 0, "吕布", "赵云");
heros
(5) ["上官婉儿", "貂蝉", "吕布", "赵云", "鲁班七号"]

另外,该方法的第一个参数可以为负数(但是第二个不能,因为它表示长度),表示从数组末端开始计数,开始计数的值为“-1”。

heros.splice(-1, 1);
heros
(4) ["上官婉儿", "貂蝉", "吕布", "赵云"]

如果只有第一个参数,则表示从指定位置删除到末尾:

heros.splice(1);
heros
["上官婉儿"]

友情提示:到目前位置,我们已经学了 splitslicesplice 三个方法,这三个方法特别容易混淆,所以一定要注意区分。

6. 截取元素

截取数组元素与字符串截取类似,使用 slice 方法,该方法语法形式如下:

arr.slice(startIdx, endIdx);

语法解读:

  • startIdx:起始位置(从下标0开始)
  • endIdx:结束位置,不包括结束位置的元素,如果省略该参数,则表示从头截取到末尾。
var heros = ["上官婉儿", "李白", "嬴政", "貂蝉", "兰陵王"];
var res = heros.slice(1, 4);
res
(3) ["李白", "嬴政", "貂蝉"]

var res = heros.slice(1);
res
(4) ["李白", "嬴政", "貂蝉", "兰陵王"]

除此之外,该方法还可以利用原型链中的 call() 方法将一个类似数组转化成真正的数组,其语法形式为:

Array.prototype.slice.call(obj);

代码示例:

<ul class="list">
    <li>LIST-1</li>
    <li>LIST-2</li>
    <li>LIST-3</li>
    <li>LIST-4</li>
    <li>LIST-5</li>
</ul>
// 1. 获取所有的.list下li标签
var lis = document.querySelectorAll(".list li");
console.log(lis);
/*
NodeList(5) [li, li, li, li, li]
0: li
1: li
2: li
3: li
4: li
length: 5
__proto__: NodeList
*/
// 2. 企图在lis对象上调用push方法
// lis.push("<li></li>");

// 3. 程序会报错,因为lis本质上不是数组而是NodeList对象
// Uncaught TypeError: lis.push is not a function

// 4. 如果想要调用push方法添加内容,需将类似数组转为真正的数组
lis = Array.prototype.slice.call(lis);
console.log(lis);
/*
(5) [li, li, li, li, li]
0: li
1: li
2: li
3: li
4: li
length: 5
__proto__: Array(0)
*/
// 5. lis已经被转换成真正的输入,所以可以调用push方法了
lis.push("<li></li>");
console.log(lis);

将一个类似数组转换为真正的数组的意义在于,类似数组不具有数组的方法,直接对一个类似数组使用数组的方法浏览器会报错,而很多时候我们的操作只有通过使用数组的方法才能完成,或用数组的方法完成才是最佳的选择。

7. 数组排序

在数组中排序使用 sort() 方法。

7.1. 数字排序

sort() 方法对数组成员进行升序排序,默认是按照ASC II 码顺序排序的,排序后,原数组将被改变。

var arr = [1, "b", 3, 2, "A", "a", "B"];
arr.sort();
arr
(7) [1, 2, 3, "A", "B", "a", "b"]

sort() 的排序原理是根据字符一一对应进行排序,首先会比较第1个字符,如果第1个字符相等,则继续比较第2个字符,以此类推….

var arr = [1, 12, 7, 5];
arr.sort();
arr
(4) [1, 12, 5, 7]

这样一来,就会导致排序不精确。若要对这样的数组实现数值大小的排序(实际项目中更多地是需要这样的结果),可以通过给 sort() 方法配置一个函数参数来实现,函数中需要配置两个参数,使该方法能实现一个差值排序的算法。用第一个参数减去第二个参数,得到的是一个升序数组;用第二个参数减去第一个参数,得到的是一个降序的数组。如例:

var nums = [1, 8, 13, 30]; 

// 1、升序
var ascending = nums.sort(function(a1, a2) {
	return a1 - a2;
});
console.log(ascending); // [1, 8, 13, 30]


// 2、降序
var descending = nums.sort(function(a1, a2) {
	return a2 - a1;
})
console.log(descending); // [30, 13, 8, 1]

通过示例可以发现,得出的排序结果已经不再是按照ASC II中首字符的前后顺序来排序了,而是按照数学中数字的大小来进行的排序。而且当 sort() 方法内的函数的两个参数在执行差值运算的位置发生变化后,得出结果的排序升降关系也不一样。

7. 2.对象排序

那么如果数组里面的元素是对象类型呢?我们又该如何处理,我们俩看一个示例:

var stus = [
    {stuNum: 1101, name: '曹操', score: 95},
    {stuNum: 1102, name: '吕布', score: 50},
    {stuNum: 1103, name: '貂蝉', score: 65},
    {stuNum: 1104, name: '赵云', score: 70}
];
var sortArr = stus.sort();
console.log(sortArr);

/*
(4) [{…}, {…}, {…}, {…}]
0: {stuNum: 1101, name: "曹操", score: 95}
1: {stuNum: 1102, name: "吕布", score: 50}
2: {stuNum: 1103, name: "貂蝉", score: 65}
3: {stuNum: 1104, name: "赵云", score: 70}
length: 4
__proto__: Array(0)
*/

通过访问遍历数组元素可以发现,数组内这四个对象的位置完全没有发生改变。其实要实现这个需求也是有办法的,就是同样需要给“sort()”方法配置一个函数作为参数来实现,实现思路和对数组排序时配置函数一致。

var sortArr = stus.sort(function(obj1, obj2) {
    return obj2.score - obj1.score;
});
console.log(sortArr);
/*
(4) [{…}, {…}, {…}, {…}]
0: {stuNum: 1101, name: "曹操", score: 95}
1: {stuNum: 1104, name: "赵云", score: 70}
2: {stuNum: 1103, name: "貂蝉", score: 65}
3: {stuNum: 1102, name: "吕布", score: 50}
length: 4
__proto__: Array(0)
*/

7.3.字符串排序

var strs = ["Apple", "Banana", "Angular", "Orange"];
strs.sort(function(a, b) {
	return a.localeCompare(b);
});
strs
(4) ["Angular", "Apple", "Banana", "Orange"]

7.4.中文排序

var  citys = ["上海","北京","杭州", "成都"];
citys.sort(function(a, b) {
	return a.localeCompare(b, "zh-CN");
});
citys
(4) ["北京", "成都", "杭州", "上海"]

8. 数组倒序

reverse() 方法的作用是将已有数组倒序排列,并返回改变后的数组。该方法同样会改变原数组。

var arr = [1, 2, 3, 4];
arr
arr.reverse();
(4) [4, 3, 2, 1]

需要注意的是该方法不具备 sort() 方法那样对数组排序的能力,它只是单纯地将当前的数组的元素进行先后顺序地调转而已。所以,在对一个有序(已经完成升序或降序排序)的数组进行反向排序的时候,会比 sort() 方法通过配置函数的参数,再对函数的参数进行位置调整来计算差值这种方式的性能要高很多。

9. 遍历修改数组元素

map 方法对数组的所有成员依次调用一个函数,根据函数结果返回一个新数组。该方法不会改变原来的数组。语法形式:

arr.map(function(item, index, arr) {})

参数解读:

  • item:每一个数组元素
  • index:数组元素对应的下标,可选参数
  • arr:原始数组,可选参数

提示:函数参数的名字是可以随意命名的。

var heros = ["阿珂", "李白", "貂蝉"];

heros.map(function(item, index, arr) {
	console.log(`item:${item}, index:${index}, arr: ${arr}`);
});

/*
VM1609:2 item:阿珂, index:0, arr: 阿珂,李白,貂蝉
VM1609:2 item:李白, index:1, arr: 阿珂,李白,貂蝉
VM1609:2 item:貂蝉, index:2, arr: 阿珂,李白,貂蝉
*/

map方法可以操作/修改数组元素。比如有一个数组存储了商品的价格,现在通过map方法给每个数字前面加上一个 ¥ 符号。

var prices = [66, 99, 85.5, 32];
prices.map(function(price, index, arr) {
	return ${price}`;
});
(4) ["¥66", "¥99", "¥85.5", "¥32"]

10. 数组遍历

数组遍历就是依次获取数组的每一个元素进行相应的操作,js提供了很多遍历数组元素的方法,如下所示:

方法说明
for通过for循环遍历数组元素,可以把计数器 i 当成数组元素的下标来访问
for-in遍历的是下标
for-of直接遍历数据
forEachforEach是数组对象提供的方法,类似于map方法,唯一的区别在于forEach没有返回值,只是单纯的遍历数据。

示例参考:

var heros = ["阿珂", "李白", "貂蝉"];

// 1. for
for(var i = 0, len = heros.length; i < len; i++) {
	console.log(heros[i]);
}

// 2. for-in
for(var index in heros) {
	console.log(heros[index]);
}

// 3. for-of
for(var hero of heros) {
	console.log(hero);
}

// 4. forEach
heros.forEach(function(hero, index, arr) {
	console.log(hero, index, arr);
});
VM2543:2 阿珂 0 (3) ["阿珂", "李白", "貂蝉"]
VM2543:2 李白 1 (3) ["阿珂", "李白", "貂蝉"]
VM2543:2 貂蝉 2 (3) ["阿珂", "李白", "貂蝉"]

11. 过滤元素

filter 方法的主要作用是过滤数组元素,它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。和之前的 mapforEach 方法一样,该方法的函数仍旧支持3个参数,参数位和之前的这两个方法也是一样的,分别表示:数组元素、元素下标和原数组。

先看一个简单的示例,过滤数组元素中的偶数:

var arr = [1, 2, 3, 4, 5];
var res = arr.filter(function(item, index, arr) {
	return item % 2 == 0;
});
res
(2) [2, 4]

12. 查询数组元素

  • indexOf() 方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回**-1**。
  • lastIndexOf() 方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1。
  • includes() 方法用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回false。
  • find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
  • findIndex 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。
var arr = [1, 2, 3, 2, 5];

arr.indexOf(2);     // 1
arr.lastIndexOf(2); // 3
arr.includes(3); // true

var res = arr.find(function(item) {
    return item > 2
});
console.log(res); // 3

var index = arr.findIndex(function(item) {
    return item == 3;
});
console.log(index);

13. 数组转字符串

join() 方法可以将数组转为字符串

var name = ["张三", "李四"];
var nameStr = name.join(","); // "张三,李四"

14. reduce

此方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。

arr.reduce((prev, cur, index, arr) => {
    return pre + cur;
}, initialValue);

提示:

  1. prev 为上一次回调函数执行的返回值
  2. 如果没有提供 initialValue 时,reduce 会从索引1的地方开始执行 callback 方法,跳过第一个索引。如果提供initialValue,从索引0开始。一般来说我们提供初始值通常更安全。

# 高级用法

1). 计算数组中每个元素出现的次数

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

let nameNum = names.reduce((pre,cur)=>{
  if(cur in pre){
    pre[cur]++
  }else{
    pre[cur] = 1 
  }
  return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

2). 数组去重

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

3).对象里的属性求和

var result = [
    {
        subject: 'math',
        score: 10
    },
    {
        subject: 'chinese',
        score: 20
    },
    {
        subject: 'english',
        score: 30
    }
];

var sum = result.reduce(function(prev, cur) {
    return cur.score + prev;
}, 0);
console.log(sum) //60

六、链式使用

在数组的方法中,除了可以实现数组操作方法的嵌套,若所用方法返回的仍旧是一个数组的话,还可以使用方法链来完成一个特定的功能。

var arr = [1, 2, 3, 5, 4];
var res = arr.sort().reverse().map(function(item){
    return item * 2;
});
console.log(res);// [ 10, 8, 6, 4, 2 ]

像这种由如链条一般的调用方法,被称为 链式调用

七、拓展

1、数组去重

// 1. 
var nums   = [1, 2, 3, 1, 3, 4, 5, 4];
var tmpArr = [];
for(var i = 0; i < nums.length; i++) {
    var index = tmpArr.indexOf(nums[i]);
    if(index == -1) {
        tmpArr.push(nums[i]);
    }
}
console.log(tmpArr);
// 2.
var nums   = [1, 2, 3, 1, 3, 4, 5, 4];
var resArr = [...new Set(nums)];
console.log(resArr);
// 3. 
var nums   = [1, 2, 13, 1, 13, 4, 5, 4];
var tmpArr = [];
var resArr = [];
for(var i = 0; i < nums.length; i++) {
    if(tmpArr[nums[i]] != 1) {
        resArr.push(nums[i]);
    }
    tmpArr[nums[i]] = 1; 
}
console.log(tmpArr);
console.log(resArr);
// 4. 
Array.prototype.removeDuplicate = function () {
    var json = {};
    var arr  = [];
    for(var i = 0, len = this.length; i < len; i++) {
        if(!json[this[i]]) {
            json[this[i]] = true;
            arr.push(this[i]);
        }
    }
    return arr;
}
var result = [1, 2, 3, 3, 4, 5].removeDuplicate();
console.log(result); // [1, 2, 3, 4, 5]
// 5.
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

2. 判断数组的方法

Object.prototype.toString.call(target)
Object.prototype.toString.call(target)
Array.isArray()
target.constructor === Array
target instanceof Array

instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。使用 instanceof 判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false

提示:instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。

八、课后作业

1. 定义一个数组,随机获取20个0~100的整数,要求如下(结果要取四舍五入):
- 计算最大值、最小值
- 计算数组元素之和、平均值
- 将数组元素和平均值的差值组成一个新的数组并按升序排序
> 提示:
- Math.random():获取0~1的随机数
- Math.round() :四舍五入

2. 去除数组[1, 3, 2, 4, 3]中重复的元素