盒子
盒子
文章目录
  1. 1. 数组的简单概念
    1. 1.1 数组是什么呢?
    2. 1.2 数组如何定义
      1. 1.2.1 构造函数不写参数
      2. 1.2.2 构造函数写1个正整数参数
      3. 1.2.3 构造函数参数是一个非正整数(字符串、boolean、对象等其他值)
      4. 1.2.4 构造函数写多个参数
      5. 1.2.5 构造函数参数是非正整数,报错
      6. 1.2.6 数组定义的正确方法
  2. 2. 数组的length属性解疑
    1. 2.1 把数组清空的方法
    2. 2.2 有趣的一点
  3. 3. 伪数组(array-like object)
    1. 3.1 数组的本质
  4. 4. 数组实例的常见简单的方法(可以无参或者参数很简单)
    1. 4.1 判断数组还是对象
    2. 4.2 valueOf(),toString()
    3. 4.3 push()
    4. 4.4 pop()
    5. 4.5 join()
    6. 4.6 concat()
    7. 4.7 shift()
    8. 4.8 unshift()
    9. 4.9 reverse()
    10. 4.10 slice()
    11. 4.11 splice()
    12. 4.12 indexOf(),lastIndexOf()
  5. 5. 数组实例的常见复杂的方法(参数是另一个函数)
    1. 5.1 sort()
    2. 5.2 map()
    3. 5.3 forEach()
    4. 5.4 filter()
    5. 5.5 reduce()
    6. 5.6 几个方法组合使用
    7. 5.7 some(),every()
  6. 6. 上述数组的方法的使用总结
    1. 6.1 改变原数组的方法
    2. 6.2 不改变原数组的方法
    3. 6.3 数组的遍历

JavaScript标准库之数组

学习任何编程语言,数组都是绕不过去的坎,每个编程语言都在其标准库里面内置了功能强大的Array对象。通过参考阮一峰教程和MDN,我把数组常见的方法以及一些误区总结如下,内容较多,而且会继续补充,希望这一篇文章可以把数组的知识一网打尽。

1. 数组的简单概念

1.1 数组是什么呢?

编程总要和数据打交道,常见的普通的数据由基本数据类型可以定义,一些具有多重属性、内容的数据就需要复杂的数据类型去定义,也就是对象来定义,数组也是对象的一种。

  • 为了方便理解,我们可以认为数组是具有一定顺序的复杂数据的组合(与对象的无序区别),每个位置对应一个索引,索引从0开始,具有length属性,而且length属性是可变的

    1.2 数组如何定义

  • 第一种方法是通过Array构造函数来定义(该方法并不常用)
1
2
3
4
5
6
var arr1 = new Array(3)
undefined
arr1
(3) [empty × 3]
length: 3
__proto__: Array(0)

以上是控制台打印结果,构造了一个长度为3的、每个元素为空的数组。


以上的写法有个小bug
虽然元素为空,但是正常来说,索引应该存在的,但是事实是 索引竟然不存在

1
2
3
4
5
6
7
8
9
10
11
arr1[0]
undefined
arr1[1]
undefined
arr1[2]
undefined
0 in arr1
false
1 in arr1
false
2 in arr1

索引0、1、2处是undefined,完全合理,但是索引不存在,很奇怪


而且new不写也是一样的结果。

1
2
3
4
5
6
var arr2 = Array(3)
undefined
arr2
(3) [empty × 3]
length: 3
__proto__: Array(0)
  • 但是采用构造函数的这种方法容易产生一些歧义,不同的参数个数,会产生不同的结果。
    1.2.1 构造函数不写参数
1
2
3
4
5
6
var arr3 = new Array
undefined
arr3
[]
length:0
__proto__:Array(0

此时构造出空的数组,而且发现构造函数的()写不写都可以

1.2.2 构造函数写1个正整数参数

那这个正整数参数就是构造出来的数组的长度。

1.2.3 构造函数参数是一个非正整数(字符串、boolean、对象等其他值)
1
2
3
4
5
6
7
var arr = new Array('jjj')
undefined
arr
["jjj"]
0: "jjj"
length: 1
__proto__: Array(0)
1
2
3
4
5
6
7
var arr = new Array(false)
undefined
arr
[false]
0: false
length: 1
__proto__: Array(0)
1
2
3
4
5
6
7
var arr = new Array({0: '我是一个对象'})
undefined
arr
[{…}]
0: {0: "我是一个对象"}
length: 1
__proto__: Array(0)

这个非正整数就是数组的内容

1.2.4 构造函数写多个参数
1
2
3
4
5
6
7
8
var arr4 = new Array(1, 2)
undefined
arr4
(2) [1, 2]
0: 1
1: 2
length: 2
__proto__: Array(0)

此时直接构造出0索引是元素1、1索引是元素2的数组对象。

1
2
3
4
5
6
7
8
9
10
var arr4 = new Array('aa', 'ff', 10, 0)
undefined
arr4
(4) ["aa", "ff", 10, 0]
0: "aa"
1: "ff"
2: 10
3: 0
length: 4
__proto__:Array(0)

即多参数时,所有参数都是返回的新数组的成员

1.2.5 构造函数参数是非正整数,报错
1
2
3
4
5
6
7
8
new Array(-1)
VM376:1 Uncaught RangeError: Invalid array length
at <anonymous>:1:1
(anonymous) @ VM376:1
new Array(3.2)
VM377:1 Uncaught RangeError: Invalid array length
at <anonymous>:1:1
(anonymous) @ VM377:1
1.2.6 数组定义的正确方法

综上所述,其实实际中直接用字面量定义数组

1
2
3
4
5
6
7
8
9
10
11
12
var arr = ['这样子', '定义', 'is', true, 1, {'good': '我是数组索引为5的元素的值'}]
undefined
arr
(6) ["这样子", "定义", "is", true, 1, {…}]
0: "这样子"
1: "定义"
2: "is"
3: true
4: 1
5: {good: "我是数组索引为5的元素的值"}
length: 6
__proto__:Array(0)

2. 数组的length属性解疑

如果你是初学者,一定要记住数组的length属性和里面的元素个数无关,爱几个元素几个元素,length并不是计数的作用。这是我自学是对数组长度最大的误解。
正确的理解是:数组的length属性等于最大正整数索引 + 1
而数组的索引可以随便改变,那么length属性也是一个动态的值,可以变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = []
undefined
arr
[]
length:0
__proto__:Array(0)
arr[10] = '我是第10个元素,我前面没有元素,但是数组的长度绝对是11,你信不信'
"我是第10个元素,我前面没有元素,但是数组的长度绝对是11,你信不信"
arr
(11) [empty × 10, "我是第10个元素,我前面没有元素,但是数组的长度绝对是11,你信不信"]
10:"我是第10个元素,我前面没有元素,但是数组的长度绝对是11,你信不信"
length:11
__proto__:Array(0)

这个例子一开始是个空数组,长度是0,直接给他一个索引10,可以发现长度立马变为11。

1
2
3
4
arr[100] = '这次数组长度绝对是101'
"这次数组长度绝对是101"
arr.length
101

通过以上的例子,我们反向推理,把可以明白数组长度根本不连续,是动态变化的,即数组长度是可写的。唯一的不变真理是,它的长度永远等于最大索引+1。

2.1 把数组清空的方法

由以上知识可以知道数组长度可以人为改变,进而大胆的猜想,改变长度会不会把数组清空呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var arrDemo = ['this', 'is', 'test']
undefined
arrDemo
(3) ["this", "is", "test"]
0: "this"
1: "is"
2: "test"
length: 3
__proto__: Array(0)
arrDemo['length'] = 2
2
arrDemo
(2) ["this", "is"]
0: "this"
1: "is"
length: 2
__proto__: Array(0)
arrDemo['length'] = 1
1
arrDemo
["this"]
0: "this"
length: 1
__proto__: Array(0)
arrDemo['length'] = 0
0
arrDemo
[]
length: 0
__proto__: Array(0)

把数组length设为0,证明可以清空数组。

2.2 有趣的一点

由于数组本质上是对象的一种,所以我们可以为数组添加属性,但是这不影响length属性的值。
一定不要有思维定式,以为添加几个新元素,长度就会加几个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var arr = []
undefined
arr
[]
length:0
__proto__:Array(0)
arr['add'] = '我加一个新元素,长度绝对还是0'
"我加一个新元素,长度绝对还是0"
arr
[add: "我加一个新元素,长度绝对还是0"]
add: "我加一个新元素,长度绝对还是0"
length:0
__proto__:Array(0)
arr['add1'] = '我又加一个新元素,长度绝对还是0'
"我又加一个新元素,长度绝对还是0"
arr
[add: "我加一个新元素,长度绝对还是0", add1: "我又加一个新元素,长度绝对还是0"]
add: "我加一个新元素,长度绝对还是0"
add1: "我又加一个新元素,长度绝对还是0"
length: 0
__proto__:Array(0)

通过这个例子,一开始元素长度为0,只要你没添加一个正整数的索引,无论你添加多少其他元素,长度永远不会变化。

  • 注意:方括号运算符里面一定要用引号,我总是手抖忘了加。

    3. 伪数组(array-like object)

    如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    }
    undefined
    obj
    {0: "a", 1: "b", 2: "c", length: 3}
    0: "a"
    1: "b"
    2: "c"
    length: 3
    __proto__: Object

    obj[0]
    "a"
    obj[2]
    "c"

上面的对象,看着结构特别像但是绝对不是数组。
因为proto指向的就不是Array的prototype,没有指向Array的共有属性,再怎么想也只是模仿,本质不同。不具备数组的其他方法(第四部分将要列举的方法)。

3.1 数组的本质

由伪数组的问题引出真正的数组应该具备什么特点
数组.png
proto必须指向数组的公有属性才是真正的数组对象。

4. 数组实例的常见简单的方法(可以无参或者参数很简单)

4.1 判断数组还是对象

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = ['a']
undefined
Array.isArray(arr)
true
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
}
undefined
Array.isArray(obj)
false

Array.isArray()方法可以判断是不是数组对象,以前学过的instanceOf也可以判断。

1
2
3
4
arr instanceof Array
true
obj instanceof Array
false

所以现在有两个方法可以区分是数组还是对象了。

4.2 valueOf(),toString()

  • valueOf()返回数组本身

    1
    2
    3
    4
    5
    6
    var arr = ['a', 'b']
    undefined
    arr.valueOf()
    (2) ["a", "b"]
    arr.toString()
    "a,b"
  • toString()返回数组的字符串形式

    4.3 push()

    1
    2
    3
    4
    5
    6
    var arr = ['a', 'b']
    undefined
    arr.push('f')
    3
    arr
    (3) ["a", "b", "f"]

向数组的末尾添加元素,返回添加成功后的数组的长度
会改变原数组

4.4 pop()

1
2
3
4
arr.pop()
"f"
arr
(2) ["a", "b"]

删除数组的最后一个元素,并返回删除的这个元素。

1
[].pop() // undefined

  • 注意:对空数组使用pop方法,不会报错,而是返回undefined。
    这个方法会改变原数组

push() 和pop()方法一起使用可以模拟栈这个数据结构


4.5 join()

以某种形式把数组的所有成员以字符串的形式返回

1
2
3
4
arr
(2) ["a", "b"]
arr.join('-')
"a-b"

以上是以中划线的形式连接起来

1
2
arr.join()
"a,b"

如果没有规定格式,则以逗号分隔

1
2
3
4
5
6
var arr = ['a', 'rr', null, undefined]
undefined
arr
(4) ["a", "rr", null, undefined]
arr.join()
"a,rr,,"

  • 注意:如果字符串中有null和undefined的,会被转成空字符串。
    该方法不会改变原数组

    4.6 concat()

    是一个专业合并数组的方法。
    1
    2
    3
    4
    5
    6
    var arr = ['a', 'rr', null, undefined]
    undefined
    arr.concat(['rrr'])
    (5) ["a", "rr", null, undefined, "rrr"]
    arr
    (4) ["a", "rr", null, undefined]

把一个新数组添加到旧数组的后面,返回生成的新数组。
不会改变原数组

4.7 shift()

删除数组的第一个元素,并返回删除的那个元素

1
2
3
4
5
6
arr
(4) ["a", "rr", null, undefined]
arr.shift()
"a"
arr
(3) ["rr", null, undefined]

会改变原数组


push()与shift()方法结合,可以模拟队列这个数据结构


4.8 unshift()

在数组的第一个位置添加元素,并返回添加新元素后的数组长度

1
2
3
4
5
6
arr
(3) ["rr", null, undefined]
arr.unshift('ffff')
4
arr
(4) ["ffff", "rr", null, undefined]

和shift()方法的作用正好相反。
一定会改变原数组

4.9 reverse()

反转数组,返回反转后的数组

1
2
3
4
5
6
arr
(4) ["ffff", "rr", null, undefined]
arr.reverse()
(4) [undefined, null, "rr", "ffff"]
arr
(4) [undefined, null, "rr", "ffff"]

会改变原数组

4.10 slice()

提取原数组的一部分,返回一个新的数组

1
2
3
4
5
6
7
8
9
10
arr
(4) [undefined, null, "rr", "ffff"]
arr.slice(1,3)
(2) [null, "rr"]
arr.slice()
(4) [undefined, null, "rr", "ffff"]
arr.slice(1)
(3) [null, "rr", "ffff"]
arr
(4) [undefined, null, "rr", "ffff"]

arr.slice(1,3)从索引为1的位置开始截取,到索引3停止,但是不包括索引3。
arr.slice()无参是原数组的拷贝
arr.slice(1)从索引为1的位置开始截取,到末尾。

1
2
3
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]

如果slice方法的参数是负数,则表示倒数计算的位置。
上面代码中,-2表示倒数计算的第二个位置,-1表示倒数计算的第一个位置。


slice()方法可以把伪数组变成真的数组


不会改变原数组

4.11 splice()

删除原数组的一部分成员,返回被删的元素。

1
2
3
4
5
6
arr
(4) [undefined, null, "rr", "ffff"]
arr.splice(1, 3)
(3) [null, "rr", "ffff"]
arr
[undefined]

arr.splice(1,3),从索引1开始删除,删3个元素!!!
一定要注意和slice区分:splice的第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。

1
2
3
4
5
6
var arr = ['1', 'aaa', 'ff', 'aff', 1]
undefined
arr.splice(1, 3, 'wu', 999)
(3) ["aaa", "ff", "aff"]
arr
(4) ["1", "wu", 999, 1]

arr.splice(1, 3, ‘wu’, 999),从索引1开始删了3个元素,有加上两个元素,’wu’和999
同样的负数表示倒数第几个位置
会改变原数组
splice()有两个变式

  • 变式1:我只是想单纯的插入一个元素
    1
    2
    3
    var a = [1, 1, 1];
    a.splice(1, 0, 2) // []
    a // [1, 2, 1, 1]

把第二个参数设为0,就可以在第2个位置插入一个元素了

  • 变式2:我只给一个参数,就是拆分数组,为两个新数组
    1
    2
    3
    4
    5
    6
    a
    (5) [1, 1, 1111, 1, 10]
    a.splice(2)
    (3) [1111, 1, 10]
    a
    (2) [1, 1]

a.splice(2)从第三个索引处拆分这个数组。

4.12 indexOf(),lastIndexOf()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var arr = ['a', 'f', 'f', 1]
undefined
arr
(4) ["a", "f", "f", 1]
0: "a"
1: "f"
2: "f"
3: 1
length: 4
__proto__: Array(0)
arr.indexOf(1)
3
arr.indexOf('f', 3)
-1
arr.lastIndexOf('f')
2

indexOf(),返回括号里面 的元素第一次出现的位置。
如果有两个参数则是表示搜索的位置从第二个参数开始。
如果找不到该元素,则返回-1。
lastIndexOf()返回括号里面的元素最后一次出现的位置。

  • 一个MDN的实战例子:获得数组里面某个元素出现的所有位置(利用循环和返回值-1的特点)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var arr = ['a', 0, 'a', 'b', 'a'];
    var arrTemp = []; //空数组用来存储目标元素出现的所有索引
    var element = 'a';
    var index = arr.indexOf(element);
    while(index != -1){
    arrTemp.push(index);
    index = arr.indexOf(element, index + 1);
    }
    console.log(arrTemp);
    (3) [0, 2, 4] //'a'出现在0、2、4索引位置处

注意:这里有个例外
数组里面包含NaN时无法判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arr = ['a', 'f', 'f', NaN]
undefined
arr
(4) ["a", "f", "f", NaN]
0: "a"
1: "f"
2: "f"
3: NaN
length: 4
__proto__: Array(0)
arr.indexOf(NaN)
-1
arr.lastIndexOf('NaN')
-1

arr数组的第四个位置是NaN,但是无法获得索引。
因为indexOf(),lastIndexOf()是严格按照===操作符来检测的,而NaN是唯一的不与自身相等的值。

1
2
3
4
5
6
NaN === NaN
false
1 === 1
true
'a' === 'a'
true

奇葩啊,NaN与自己都不相等


5. 数组实例的常见复杂的方法(参数是另一个函数)

5.1 sort()

下面MDN的解释非常棒

sort() 方法在适当的位置对数组的元素进行排序,并返回数组。 sort 排序不一定是稳定的。默认排序顺序是根据字符串Unicode码点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var fruit = ['cherries', 'apples', 'bananas'];
fruit.sort();
// ['apples', 'bananas', 'cherries']

var scores = [1, 10, 21, 2];
scores.sort();
// [1, 10, 2, 21]
// 注意10在2之前,
// 因为在 Unicode 指针顺序中"10"在"2"之前

var things = ['word', 'Word', '1 Word', '2 Words'];
things.sort();
// ['1 Word', '2 Words', 'Word', 'word']
// 在Unicode中, 数字在大写字母之前,
// 大写字母在小写字母之前.

上述代码两点注意

  • 第一点是
    上述代码中的第二部分的[1, 10, 2, 21]是因为
    10的Unicode编码是\u0031\u0030,2的Unicode编码是\u0032,所以10排在2的前面

  • 第二点是上述代码中的第三部分的['1 Word', '2 Words', 'Word', 'word']是因为
    'Word'的Unicode编码是

  • 1
    \u0026\u0023\u0033\u0039\u003b\u0057\u006f\u0072\u0064\u0026\u0023\u0033\u0039\u003b

    'word'的Unicode编码是

  • 1
    \u0026\u0023\u0033\u0039\u003b\u0077\u006f\u0072\u0064\u0026\u0023\u0033\u0039\u003b

    所以 ‘Word’排在’word’前面。

    各种编码查询站长工具
    sort方法明显的会改变原数组啊

  • 我们通常不想使用默认的升序排列,sort方法可以传入函数来改变顺序。
    MDN的语法是arr.sort(compareFunction)
    compareFunction这个函数用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的诸个字符的Unicode位点进行排序。
    compareFunction这个函数基本的规则是传入两个参数

    1
    2
    3
    function compareNumbers(a, b) {
    return a - b;
    }
a,b参数比较 代表的意思
compareFunction(a, b) < 0 a在b之前
compareFunction(a, b) > 0 b在a之前
1
2
3
4
5
6
var a = [1, 20, 30, -7]
undefined
a
(4) [1, 20, 30, -7]
a.sort(function(a,b){return b-a})
(4) [30, 20, 1, -7]

降序排列。

  • 也可以根据具体需求来根据属性来排列
    1
    2
    3
    4
    5
    6
    var students = ['小明','小红','小花'];
    var scores = { 小明: 59, 小红: 99, 小花: 80 };
    students.sort(function(a, b){
    return scores[b] - scores[a]
    });
    (3) ["小红", "小花", "小明"]

以上是把三个学生根据成绩从大到小排列的

5.2 map()

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
不影响原数组。

1
2
3
4
5
6
7
var arr = ['aa', 'bb', 'cc']
arr.map(function(value){
return value = value + "f"
})
(3) ["aaf", "bbf", "ccf"]
arr
(3) ["aa", "bb", "cc"]

以上代码中map()方法里面传入的函数是一个把数组每个值都加上一个’f’。
每个元素末尾都加上一个’f’,然后返回这个新的数组,原数组没有任何变化的。
我初学的时候,看到上述代码反正很懵逼,这玩意咋出来的这个结果呢。琢磨了很久,还是觉得MDN的解释明白,只不过需要看个3、4遍就能明白了。
语法规范是:

1
2
3
let new_array = arr.map(function callback(currentValue, index, array) { 
// Return element for new_array
}[, thisArg])

callback
生成新数组元素的函数,使用三个参数:
currentValue
callback 的第一个参数,数组中正在处理的当前元素。
index
callback 的第二个参数,数组中正在处理的当前元素的索引。
array
callback 的第三个参数,map 方法被调用的数组。
thisArg
可选的。执行 callback 函数时 使用的this 值。
返回值
一个新数组,每个元素都是回调函数的结果。

1
2
3
4
[1, 2, 3].map(function(currentValue, index, arr){
return currentValue*index
})
(3) [0, 2, 6]

其实callback 的第三个参数可以不写,也知道调用的到底是哪个Array。

1
2
3
4
[1, 2, 3].map(function(currentValue, index){
return currentValue*index
})
(3) [0, 2, 6]

当你用map()方法的时候,callback 函数会被自动传入三个参数:数组的每一个元素,元素索引,原数组本身。既然原数组本身可以省略,那么由剩下的两个特点我们发散一下,会想到前面我们讲过,伪数组(比如字符串)也具备这两个特点会不会也能用map()方法呢,接下来做个实验。
哈哈哈哈,愚蠢的人类,你想的美,怎么可能直接使用呢,必须把伪数组转换一下的。

  • 第一种转换方法
    1
    2
    3
    4
    5
    var upper = function (str){
    return str.toUpperCase();
    };
    [].map.call('abc', upper)
    (3) ["A", "B", "C"]

以上是通过map函数的call方法间接使用

  • 第二种转换方法
    1
    2
    'abc'.split('').map(upper)
    (3) ["A", "B", "C"]

'abc'.split('')把字符串转成数组["a", "b", "c"]


至此,字符串和数组相互转化的方法,都学到了,总结如下。

  • 数组转字符串 三种方法

    1
    2
    3
    4
    5
    6
    [1, 3, 4].toString()
    "1,3,4"
    [1, 3, 4] + ''
    "1,3,4"
    [1, 3, 4].join()
    "1,3,4"
  • 字符串转数组 一种方法

    1
    2
    'abxc'.split('')
    (4) ["a", "b", "x", "c"]

在map()的最后,要注意数组的空位问题。
我们先看一个map()处理含有空位的数组的奇怪现象

1
2
3
4
5
6
7
8
var f = function(n){ return n + 1 };
undefined
[1, , 2].map(f)
(3) [2, empty, 3]
[1, undefined, 2].map(f)
(3) [2, NaN, 3]
[1, null, 2].map(f)
(3) [2, 1, 3]

可以发现[1, , 2].map(f)空位未执行map()。map方法不会跳过undefined和null,但是会跳过空位。

1
2
3
4
null + 1 = 1
true + 1 = 2
false + 1 = 1
//好奇怪

  • 用一个更直观的例子来证明map方法会跳过空位
    1
    2
    3
    4
    5
    6
    7
    Array(2).map(function (){
    console.log('enter...');
    return 1;
    })
    (2) [empty × 2]
    length: 2
    __proto__: Array(0)

本文一开始就讲了Array[2]始构造了长度为2的空数组,没有打印出enter,说明未执行map()方法。

使用 map 方法处理数组时,数组元素的范围是在 callback 方法第一次调用之前就已经确定了。在 map 方法执行的过程中:原数组中新增加的元素将不会被 callback 访问到;若已经存在的元素被改变或删除了,则它们的传递到 callback 的值是 map 方法遍历到它们的那一时刻的值;而被删除的元素将不会被访问到。


以上引入了数组的空位(hole)概念,那什么才是数组的空位呢
var a= [1, , 2] 中间就是一个空位

1
2
3
4
5
6
7
8
9
10
var a= [1, , 2]
undefined
a
(3) [1, empty, 2]
0: 1
2: 2
length: 3
__proto__: Array(0)
a[1]
undefined

可以看到,空位计入数组长度,空位可读取,但是是undefined。
delete命令可以删除数组内的一个元素

1
2
3
4
5
6
7
8
9
a
(3) [1, empty, 2]
delete a[0]
true
a
(3) [empty × 2, 2]
2: 2
length: 3
__proto__: Array(0)

delete命令删除成功,返回true,但是length不变,说明空位可以被读取到,所以用delete命令无法清空数组。目前把数组清空的唯一方法就是把length属性改为0。
换句话说length属性不能过滤空位
当使用length属性进行数组遍历时,一定要非常小心。

数组的某个位置是空位,与某个位置是undefined,是不一样的。
为什么不一样呢。

  • 如果是空位,使用数组的forEach方法(接下来重点研究)、for…in结构、以及Object.keys方法进行遍历,空位都会被跳过。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var a = [1, , , 5]
    undefined
    a
    (4) [1, empty × 2, 5]
    0: 1
    3: 5
    length: 4
    __proto__: Array(0)
    //只打印出了已经存在具体数值的1和5
    a.forEach(function(x){console.log(x)})
    1
    5
    undefined
    //只有0索引和3索引
    for (var i in a) {
    console.log(i);
    }
    0
    3
    undefined
    //只有0索引和3索引
    Object.keys(a)
    (2) ["0", "3"]
    0: "0"
    1: "3"
    length: 2
    __proto__: Array(0)
  • 如果是undefined,使用数组的forEach方法(接下来重点研究)、for…in结构、以及Object.keys方法进行遍历,不会被跳过。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var a = [undefined, undefined, undefined];

    a.forEach(function (x, i) {
    console.log(i + '. ' + x);
    });
    // 0. undefined
    // 1. undefined
    // 2. undefined

    for (var i in a) {
    console.log(i);
    }
    // 0
    // 1
    // 2

    Object.keys(a)
    // ['0', '1', '2']

上面的对比可以知道,空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过。


5.3 forEach()

该方法与map()类似,都是使数组的每个元素执行一个函数。与map()的最大区别是没有返回值,而map()返回一个新的数组。forEach()只关心数据的操作,而不关心返回值。forEach()方法传入的函数,实际上是有3个值。
MDN的语法规范

1
2
3
4
5
array.forEach(callback(currentValue, index, array){
    //do something
}, this)

array.forEach(callback[, thisArg])

参数列表的含义与map()方法的每个参数含义相同。
callback()函数的array参数,通常省略,自己要脑补上。

1
2
3
4
5
6
7
//x就是数组的每一个元素,i是每一个元素的索引
arr.forEach(function(x, i){
console.log(i + ': ' + x)
})
0: 1
1: 2
2: 3

谁去调用的forEach()方法,那么callback()里面的array就会自动传入那个数组,但是是隐藏的。和我一样的初学者,都曾怀疑过,哪里传进来的数组呢,最好的答案都在MDN的callback()函数的语法规则里面,具体的细节分析和map()的分析一样。

  • 注意: 用forEach()方法遍历数组,无法再某个条件时停止遍历,此时应该用普通的for循环
    1
    2
    3
    4
    5
    6
    7
    var arr1 = [1, 2, 3]
    undefined
    for (let i = 0; i < arr1.length; i++){
    if(arr1[i] === 2){break;}
    console.log(i)
    }
    0

上面代码中,执行到数组的第二个成员时,就会中断执行。forEach方法做不到这一点。

  • 与map()方法一样,forEach方法会跳过数组的空位。而不会跳过undefined和null。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var log = function (n) {
    console.log(n + 1);
    };

    [1, undefined, 2].forEach(log)
    // 2
    // NaN
    // 3

    [1, null, 2].forEach(log)
    // 2
    // 1
    // 3

    [1, , 2].forEach(log)
    // 2
    // 3
  • 当然了,forEach方法也可以用于类似数组的对象和字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var obj = {
    0: 1,
    a: 'hello',
    length: 1
    }

    Array.prototype.forEach.call(obj, function (value, i) {
    console.log( i + ':' + value);
    });
    // 0:1

    var str = 'hello';
    Array.prototype.forEach.call(str, function (value, i) {
    console.log( i + ':' + value);
    });
    // 0:h
    // 1:e
    // 2:l
    // 3:l
    // 4:o

对象和字符串使用foreach一定要用Array.prototype.forEach.call()的。

forEach 遍历的范围在第一次调用 callback 前就会确定。调用forEach 后添加到数组中的项不会被 callback 访问到。如果已经存在的值被改变,则传递给 callback 的值是 forEach 遍历到他们那一刻的值。已删除的项不会被遍历到。如果已访问的元素在迭代时被删除了(例如使用 shift()) ,之后的元素将被跳过

ε=(´ο`*)))唉,上面这段话啊,可以看出forEach()和map()函数如此的相似啊。

  • 举一个MDN上面的例子,一旦数组被修改了,遍历不受你的影响
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var words = ["one", "two", "three", "four"];
    words.forEach(function(word) {
    console.log(word);
    if (word === "two") {
    words.push('aaa');
    }
    });
    one
    two
    three
    four

我们发现遍历出了原来的所有元素,在forEach()开始之后的添加的'aaa'并不会遍历到。
不过MDN的例子比我的难度大多了啊。

1
2
3
4
5
6
7
8
9
10
var words = ["one", "two", "three", "four"];
words.forEach(function(word) {
console.log(word);
if (word === "two") {
words.shift();
}
});
// one
// two
// four

当到达包含值”two”的项时,整个数组的第一个项被移除了,这导致所有剩下的项上移一个位置。因为元素 “four”现在在原数组的第三个位置,three跑到了第二个位置,而此时要去遍历第三个位置,所以不会打印three。

5.4 filter()

filter方法的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组。
通俗的理解就是过滤器。callback()函数与以上两个一样,也是传入三个参数。
第一个参数是当前数组成员的值,这个是必须的。

1
2
3
4
5
6
var arr = [1, 3, 5, 7]
undefined
arr.filter(function(value){return value>5})
[7]
arr.filter(function(value){return value>1})
(3) [3, 5, 7]

可以理解为给filter()传入的函数一个规则,满足规则的才能返回。

5.5 reduce()

reduce() 方法对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。
以上是MDN的解释,挺难理解字面意思的。直接用实例来理解吧。

  • 累加求和
    1
    2
    3
    4
    5
    6
    var arr = [1, 3, 10, 6] 
    undefined
    arr.reduce(function(preSum, ele){
    return preSum + ele;
    })
    20

reduce()函数传入一个函数作为参数,函数里面传入两个参数,preSum默认是数组的第一个元素,每次都把数组的两个元素相加并返回,ele就是每个数组元素。
你也快成规定起始的累加值

1
2
3
4
arr.reduce(function(preSum, ele){
return preSum + ele;
}, 10)
30

起始的累加值是10,那么加上数组的20就是30。

  • 用reduce表示map()
    1
    2
    3
    4
    5
    6
    7
    var arr = [1, 3, 4]
    undefined
    arr.reduce(function(arr, n){
    arr.push(n*2)
    return arr
    }, [])//[]空数组作为一个初始值
    (3) [2, 6, 8]

利用reduce()完成了map()一样的功能

  • 用reduce表示filter()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var arr = [1, 3, 4, 10, 30]
    undefined
    arr.reduce(function(arr, n){
    if(n>3){
    arr.push(n)
    }
    return arr
    }, [])
    (3) [4, 10, 30]

如果原数组里面的值大于3,就放到新的数组里面。和filter()道理一样。

  • 计算数组里面技术的和
    var a = [1,2,3,4,5,6,7,8,9]
    计算所有奇数的和
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = [1,2,3,4,5,6,7,8,9]
    a.reduce(function(sum, n){
    if(n % 2 === 0){
    return sum
    } else{
    return sum + n
    }
    })
    25

先判断一下,再把奇数相加

5.6 几个方法组合使用

  • 计算数组的偶数和
    给定一个 数组 var a = [1,2,3,4,5,6,7,8,9]
  1. 获取所有偶数
  2. 得到所有偶数的平方
    1
    2
    3
    4
    5
    6
    7
    a.filter(function(n){
    if (n %2 ===0){
    return n
    }
    }).map(function(n){
    return n*n
    })//[4,16,36,64]

先调用filter()获得所有偶数,再调用map()获得所有偶数平方和

5.7 some(),every()

some() 方法测试数组中的某些元素是否通过由提供的函数实现的测试。
传入的参数也是一个callback()函数,callback 被调用时传入三个参数:元素的值,元素的索引,被遍历的数组。其实一般只要发现时传入callback()函数,基本都是这些参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arr
(5) [1, 3, 4, 10, 30, notNumber: "not a number"]
0: 1
1: 3
2: 4
3: 10
4: 30
notNumber: "not a number"
length: 5
__proto__: Array(0)
arr.some(function(value, index){
return index > 5
})
false
arr.some(function(value, index){
return index > 3
})
true

some()方法的作用是只要数组中的某个元素满足传入的函数的要求就返回true

every() 方法测试数组的所有元素是否都通过了指定函数的测试。

1
2
3
4
5
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem >= 3;
});
// false

every()是要求数组的所有元素都满足传入的函数的要求才返回true

  • 注意:对于空数组,some方法返回false,every方法返回true,回调函数都不会执行。
    1
    2
    3
    4
    5
    6
    7
    8
    function isEven(x) { return x % 2 === 0 }
    undefined

    [].every(isEven)
    true

    [].some(isEven)
    false

对上面的结果,我又有什么办法呢,只能选择背过呗。
这两个方法都不改变原数组

6. 上述数组的方法的使用总结

数组的上述方法种类繁多,不过有几个特点很明显,一些方法会改变原数组,一些方法不会改变原数组,我以这个细节把上述方法分类如下

6.1 改变原数组的方法

方法名字 方法作用
push() 在元素末尾添加元素,返回添加新元素后的数组长度
pop() 删除数组末尾的元素,返回删除的那个元素。与push()方法一起模拟栈这个数据结构
shift() 删除数组的第一个元素,返回删除的那个元素。与push()方法结合,模拟队列这个数列这个数据结构
unshift() 在数组的起始位置添加新元素,返回添加新元素后的数组长度
reverse() 把数组的每一个元素的位置互换,返回翻转后的数组
splice() 根据方法传入的参数删除原数组的部分元素,返回被删除的元素。可以用来拆分数组
indexOf(),lastIndexOf() 返回括号里面 的元素第一次出现和最后一次出现的位置。NaN元素无法获得位置
sort() 默认按照数组元素的Unicode码点排序,可以自己传入函数,规定排序准则

6.2 不改变原数组的方法

方法名字 方法作用
join() 以某种形式把数组的所有元素以字符串的形式返回,默认以逗号分隔,返回生成的新数组
concat() 专业合并数组,把新数组添加到旧数组的后面,返回生成的新数组
slice() 根据方法传入的参数提取原数组的部分,返回提取的这个新数组也可以用来把伪数组变成真数组
map() 必须传入一个callback()函数,数组的每一个元素执行这个函数,返回执行回调函数后的新数组。该方法会跳过空位
forEach() 必须传入一个callback()函数,数组的每一个元素执行这个函数。没有返回值,无法终止循环
filter() 必须传入一个callback()函数,数组的每一个元素执行这个函数,返回结果为true的成员组成一个新数组返回
reduce() 对数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。具体理解看例子吧
some() 只要数组中的某个元素满足传入的函数的要求就返回true
every() 数组的所有元素都满足传入的函数的要求才返回true

正是因为以上的方法对原数组不造成影响,所以我们可以组合使用filter()、map()先过滤再匹配。

6.3 数组的遍历

对于有序无序的数据,我们有时候会希望获得所有的key或者value,数组对这个需求尤甚。
一般来说,数组的遍历有三种方法

  • for…in循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var arr = [1, 3, 4, 10, 30]
    undefined
    for (var key in arr){
    console.log(arr[key])
    }
    1
    3
    4
    10
    30
  • 切忌把arr[key]手抖写成了arr.key。因为arr.key等同于arr[‘key’],很明显数组没有这个名字叫key的键。
    for…in循环有个弊端就是它会把非数字的索引也打印出来

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    arr
    (5) [1, 3, 4, 10, 30]
    arr.notNumber = 'not a number'
    "not a number"
    arr
    (5) [1, 3, 4, 10, 30, notNumber: "not a number"]

    for (var key in arr){
    console.log(key + ':' + arr[key])
    }
    0: 1
    1: 3
    2: 4
    3: 10
    4: 30
    notNumber: not a number

如果我们只关心数组的数字索引,用传统的下面的传统for循环

  • 传统for循环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    arr
    (5) [1, 3, 4, 10, 30, notNumber: "not a number"]
    for (let i = 0; i < arr.length; i++){
    console.log(i + ':' + arr[i])
    }
    0:1
    1:3
    2:4
    3:10
    4:30

这种方法其实是我们人为规定了只遍历数字索引,O(∩_∩)O哈哈~

  • forEach()循环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    arr
    (5) [1, 3, 4, 10, 30, notNumber: "not a number"]
    arr.forEach(function(value, index){
    console.log(index + ':' + value)
    })
    0:1
    1:3
    2:4
    3:10
    4:30

这种方法也不会遍历非数字的索引。