学习任何编程语言,数组都是绕不过去的坎,每个编程语言都在其标准库里面内置了功能强大的Array对象。通过参考阮一峰教程和MDN,我把数组常见的方法以及一些误区总结如下,内容较多,而且会继续补充,希望这一篇文章可以把数组的知识一网打尽。
1. 数组的简单概念
1.1 数组是什么呢?
编程总要和数据打交道,常见的普通的数据由基本数据类型可以定义,一些具有多重属性、内容的数据就需要复杂的数据类型去定义,也就是对象来定义,数组也是对象的一种。
- 为了方便理解,我们可以认为数组是具有一定顺序的复杂数据的组合(与对象的无序区别),每个位置对应一个索引,索引从0开始,具有length属性,而且length属性是可变的。
1.2 数组如何定义
- 第一种方法是通过Array构造函数来定义(该方法并不常用)
var arr1 = new Array(3)
undefined
arr1
(3) [empty × 3]
length: 3
__proto__: Array(0)
以上是控制台打印结果,构造了一个长度为3的、每个元素为空的数组。
以上的写法有个小bug
虽然元素为空,但是正常来说,索引应该存在的,但是事实是 索引竟然不存在
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不写也是一样的结果。
var arr2 = Array(3)
undefined
arr2
(3) [empty × 3]
length: 3
__proto__: Array(0)
- 但是采用构造函数的这种方法容易产生一些歧义,不同的参数个数,会产生不同的结果。
1.2.1 构造函数不写参数
var arr3 = new Array
undefined
arr3
[]
length:0
__proto__:Array(0
此时构造出空的数组,而且发现构造函数的()写不写都可以
1.2.2 构造函数写1个正整数参数
那这个正整数参数就是构造出来的数组的长度。
1.2.3 构造函数参数是一个非正整数(字符串、boolean、对象等其他值)
var arr = new Array('jjj')
undefined
arr
["jjj"]
0: "jjj"
length: 1
__proto__: Array(0)
var arr = new Array(false)
undefined
arr
[false]
0: false
length: 1
__proto__: Array(0)
var arr = new Array({0: '我是一个对象'})
undefined
arr
[{…}]
0: {0: "我是一个对象"}
length: 1
__proto__: Array(0)
这个非正整数就是数组的内容
1.2.4 构造函数写多个参数
var arr4 = new Array(1, 2)
undefined
arr4
(2) [1, 2]
0: 1
1: 2
length: 2
__proto__: Array(0)
此时直接构造出0索引是元素1、1索引是元素2的数组对象。
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 构造函数参数是非正整数,报错
new Array(-1)
VM376:1 Uncaught RangeError: Invalid array length
at :1:1
(anonymous) @ VM376:1
new Array(3.2)
VM377:1 Uncaught RangeError: Invalid array length
at :1:1
(anonymous) @ VM377:1
1.2.6 数组定义的正确方法
综上所述,其实实际中直接用字面量定义数组
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属性也是一个动态的值,可以变化。
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。
arr[100] = '这次数组长度绝对是101'
"这次数组长度绝对是101"
arr.length
101
通过以上的例子,我们反向推理,把可以明白数组长度根本不连续,是动态变化的,即数组长度是可写的。唯一的不变真理是,它的长度永远等于最大索引+1。
2.1 把数组清空的方法
有以上知识可以知道数组长度可以人为改变,进而大胆的猜想,改变长度会不会把数组清空呢?
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属性的值。
一定不要有思维定式,以为添加几个新元素,长度就会加几个。
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属性,那么这个对象就很像数组,语法上称为“类似数组的对象”
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 数组的本质
由伪数组的问题引出真正的数组应该具备什么特点
proto必须指向数组的公有属性才是真正的数组对象。
4. 数组实例的常见简单的方法(可以无参或者参数很简单)
4.1 判断数组还是对象
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也可以判断。
arr instanceof Array
true
obj instanceof Array
false
所以现在有两个方法可以区分是数组还是对象了。
4.2 valueOf(),toString()
- valueOf()返回数组本身
var arr = ['a', 'b']
undefined
arr.valueOf()
(2) ["a", "b"]
arr.toString()
"a,b"
- toString()返回数组的字符串形式
4.3 push()
var arr = ['a', 'b']
undefined
arr.push('f')
3
arr
(3) ["a", "b", "f"]
向数组的末尾添加元素,返回添加成功后的数组的长度
会改变原数组
4.4 pop()
arr.pop()
"f"
arr
(2) ["a", "b"]
删除数组的最后一个元素,并返回删除的这个元素。
[].pop() // undefined
- 注意:对空数组使用pop方法,不会报错,而是返回undefined。
这个方法会改变原数组
push() 和pop()方法一起使用可以模拟栈的数据结构
4.5 join()
以某种形式把数组的所有成员以字符串的形式返回
arr
(2) ["a", "b"]
arr.join('-')
"a-b"
以上是以中划线的形式连接起来
arr.join()
"a,b"
如果没有规定格式,则以逗号分隔
var arr = ['a', 'rr', null, undefined]
undefined
arr
(4) ["a", "rr", null, undefined]
arr.join()
"a,rr,,"
- 注意:如果字符串中有null和undefined的,会被转成空字符串。
该方法不会改变原数组
4.6 concat()
是一个专业合并数组的方法。
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()
删除数组的第一个元素,并返回删除的那个元素
arr
(4) ["a", "rr", null, undefined]
arr.shift()
"a"
arr
(3) ["rr", null, undefined]
会改变原数组
push()与shift()方法结合,可以模拟队列的数据结构
4.8 unshift()
在数组的第一个位置添加元素,并返回添加新元素后的数组长度
arr
(3) ["rr", null, undefined]
arr.unshift('ffff')
4
arr
(4) ["ffff", "rr", null, undefined]
和shift()方法的作用正好相反。
一定会改变原数组
4.9 reverse()
反转数组,返回反转后的数组
arr
(4) ["ffff", "rr", null, undefined]
arr.reverse()
(4) [undefined, null, "rr", "ffff"]
arr
(4) [undefined, null, "rr", "ffff"]
会改变原数组
4.10 slice()
提取原数组的一部分,返回一个新的数组
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的位置开始截取,到末尾。
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"]
如果slice方法的参数是负数,则表示倒数计算的位置。
上面代码中,-2表示倒数计算的第二个位置,-1表示倒数计算的第一个位置。
slice()方法可以吧伪数组变成真的数组
不会改变原数组
4.11 splice()
删除原数组的一部分成员,返回被删的元素。
arr
(4) [undefined, null, "rr", "ffff"]
arr.splice(1, 3)
(3) [null, "rr", "ffff"]
arr
[undefined]
arr.splice(1,3),从索引1开始删除,删3个元素!!!
一定要注意和slice区分:splice的第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
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:我只是想单纯的插入一个元素
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
把第二个参数设为0,就可以在第2个位置插入一个元素了
- 变式2:我只给一个参数,就是拆分数组,为两个新数组
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()
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的特点)
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时无法判断
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是唯一的不与自身相等的值。
NaN === NaN
false
1 === 1
true
'a' === 'a'
true
奇葩啊,NaN与自己都不相等
5. 数组实例的常见复杂的方法(参数是另一个函数)
5.1 sort()
下面MDN的解释非常棒
sort()
方法在适当的位置对数组的元素进行排序,并返回数组。 sort 排序不一定是稳定的。默认排序顺序是根据字符串Unicode码点。
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编码是\u0026\u0023\u0033\u0039\u003b\u0057\u006f\u0072\u0064\u0026\u0023\u0033\u0039\u003b
'word'
的Unicode编码是\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
这个函数基本的规则是传入两个参数
function compareNumbers(a, b) {
return a - b;
}
a,b参数比较 | 代表的意思 |
---|---|
compareFunction(a, b) < 0 | a在b之前 |
compareFunction(a, b) > 0 | b在a之前 |
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]
降序排列。
- 也可以根据具体需求来根据属性来排列
var students = ['小明','小红','小花'];
var scores = { 小明: 59, 小红: 99, 小花: 80 };
students.sort(function(a, b){
return scores[b] - scores[a]
});
(3) ["小红", "小花", "小明"]
以上是把三个学生根据成绩从大到小排列的
5.2 map()
map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
不影响原数组。
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遍就能明白了。
语法规范是:
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].map(function(currentValue, index, arr){
return currentValue*index
})
(3) [0, 2, 6]
其实callback 的第三个参数可以不写,也知道调用的到底是哪个Array。
[1, 2, 3].map(function(currentValue, index){
return currentValue*index
})
(3) [0, 2, 6]
当你用map()方法的时候,callback 函数会被自动传入三个参数:数组的每一个元素,元素索引,原数组本身。既然原数组本身可以省略,那么由剩下的两个特点我们发散一下,会想到前面我们讲过,伪数组(比如字符串)也具备这两个特点会不会也能用map()方法呢,接下来做个实验。
哈哈哈哈,愚蠢的人类,你想的美,怎么可能直接使用呢,必须把伪数组转换一下的。
- 第一种转换方法
var upper = function (str){
return str.toUpperCase();
};
[].map.call('abc', upper)
(3) ["A", "B", "C"]
以上是通过map函数的call方法间接使用
- 第二种转换方法
'abc'.split('').map(upper)
(3) ["A", "B", "C"]
'abc'.split('')
把字符串转成数组["a", "b", "c"]
至此,字符串和数组相互转化的方法,都学到了,总结如下。
- 数组转字符串 三种方法
[1, 3, 4].toString()
"1,3,4"
[1, 3, 4] + ''
"1,3,4"
[1, 3, 4].join()
"1,3,4"
- 字符串转数组 一种方法
'abxc'.split('')
(4) ["a", "b", "x", "c"]
在map()的最后,要注意数组的空位问题。
我们先看一个map()处理含有空位的数组的奇怪现象
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,但是会跳过空位。
null + 1 = 1
true + 1 = 2
false + 1 = 1
//好奇怪
- 用一个更直观的例子来证明map方法会跳过空位
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]
中间就是一个空位
var a= [1, , 2]
undefined
a
(3) [1, empty, 2]
0: 1
2: 2
length: 3
__proto__: Array(0)
a[1]
undefined
可以看到,空位计入数组长度,空位可读取,但是是undefined。
delete命令可以删除数组内的一个元素
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方法进行遍历,空位都会被跳过。
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方法进行遍历,不会被跳过。
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的语法规范
array.forEach(callback(currentValue, index, array){
//do something
}, this)
array.forEach(callback[, thisArg])
参数列表的含义与map()方法的每个参数含义相同。
callback()函数的array参数,通常省略,自己要脑补上。
//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循环
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。
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方法也可以用于类似数组的对象和字符串。
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上面的例子,一旦数组被修改了,遍历不受你的影响
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的例子比我的难度大多了啊。
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()函数与以上两个一样,也是传入三个参数。
第一个参数是当前数组成员的值,这个是必须的。
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的解释,挺难理解字面意思的。直接用实例来理解吧。
- 累加求和
var arr = [1, 3, 10, 6]
undefined
arr.reduce(function(preSum, ele){
return preSum + ele;
})
20
reduce()函数传入一个函数作为参数,函数里面传入两个参数,preSum默认是数组的第一个元素,每次都把数组的两个元素相加并返回,ele就是每个数组元素。
你也快成规定起始的累加值
arr.reduce(function(preSum, ele){
return preSum + ele;
}, 10)
30
起始的累加值是10,那么加上数组的20就是30。
- 用reduce表示map()
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()
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]
计算所有奇数的和
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]
- 获取所有偶数
- 得到所有偶数的平方
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()函数,基本都是这些参数。
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() 方法测试数组的所有元素是否都通过了指定函数的测试。
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem >= 3;
});
// false
every()是要求数组的所有元素都满足传入的函数的要求才返回true。
- 注意:对于空数组,some方法返回false,every方法返回true,回调函数都不会执行。
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循环
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循环有个弊端就是它会把非数字的索引也打印出来
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循环
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()循环
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
这种方法也不会遍历非数字的索引。