本章讲解数组.数组是js
以及多数其他编程语言的一种基础数据类型.数组是值的有序集合,其中的值叫作元素,每个元素有一个数值表示的位置,叫做索引.js
数组是无类型限制的,即数组中的元素可以是任意类型,同一数组的不同元素也可以是不同的类型.数组元素甚至可以对象或其他数组,从而可以创建复杂的数据结构,比如对象的数组或者数组的数组.js
数组是基于零且使用32位数值索引的,第一个元素的索引为0,最大可能索引值是4294967294(2^32-2),即数组最大包含4294967295个元素.js
数组是动态的,他们会按需增大或缩小,因此创建数组是无需声明一个固定大小,也无需在大小变化时重新为他们分配空间.js
数组可以是稀疏的,即元素不一定具有连续的索引,中间可能有间隙,每个js
数组都有length属性,对于非稀疏数组,这个属性保存数组中元素的个数.对于稀疏数组,length大于所有元素的最高索引.
js
数组是一种特殊的js
对象,因此数组索引更像是属性名,只不过碰巧是整数而已.本章经常会谈到数组的这种特殊性.实现通常对数组会进行特别优化,从而让访问数组索引的数组元素明显快于访问常规的对象属性.
数组从Array.prototype继承属性,这个原型上定义了很多数组操作方法,7.8节将介绍.其中很多方法都是泛型的,这意味着他们不仅可以用于真正的数组,也可以用于任何"类数组对象"7.9节将讨论数组对象.最后,js
字符串的行为类似字母数组,将在7.10节讨论
ES6增加了一批新的数组类,统称为"定型数组"(typed array).与常规js
数组不同,定型数组具有固定长度和固定的数组元素类型.定型数组具有极高的性能,支持对二进制数据的字节级访问.将11.2节介绍.
创建数组有几种方式,接下来几节将分别介绍:
迄今为止,创建数组最简单的方式就是使用数组字面量.数组字面量其实就是一对方括号中逗号分割的数组元素的列表.例如:
let empty = [];// 没有元素的数组
let primes = [2,3,5,7,11] // 有5个数值元素的数组
let misc = [1.1,true,"a",] // 3种不同类型的元素,最后还有一个逗号
数组 字面量中的值不需要是常量,可以是任意表达式:
let base = 1024
let table = [base,base+1,base+2,base+3]
数组字面量可以包含对象字面量或其他数组字面量:
let b = [[1,{x:1,y:2}],[2,{x:3,y:4}]];
如果数组字面量中连续包含多个逗号,且逗号之间没有值,则这个数值就是稀疏的(参见7.3节).这些省略了值的数组元素并不存在,但按照索引查询他们时又会返回undefined:
let count = [1,,3]//索引0和2有元素,索引1没有元素
let undefs = [,,]// 这个数组没元素但长度为2
数组字面量语法允许末尾出现逗号,因此[,]的长度是2而不是3
在ES6及之后的版本中,可以使用扩展操作符…在一个数组字面量中包含另一个数组的元素:
let a = [1,2,3];
let b = [0,...a,4];// b == [0,1,2,3,4]
这里的三个点会"拓展"数组a,因而他的元素变成了要创建的数组字面量的元素.可以把…a想象成代表数组a的元素,这些元素依次出现在了包含他们的数组字面量中(注意,虽然我们把这个三个点称为拓展操作符,但他们实际上并不是操作符,因为只能在数组字面量和后面介绍的函数调用中使用他们)
拓展操作符是创建数组(浅)副本的一种便捷方式:
let original = [1,2,3];
let copy = [...original]
copy[0] = 0 // 修改copy不会影响original
original[0] // 1
拓展操作符适用于任何可迭代对象(可迭代对象可以使用for/of循环遍历,5.4.4节已经看到过了,第十二章还将看到更多例子).字符串是可迭代对象,因此可以使用拓展操作符把任意字符串转换为单个字符的数组:
let digits = [..."0123456789ABCDEF"];
digits // ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
集合对象(参见11.1.1节)是可迭代的,因此要去除数组中的重复元素,一种便捷方式就是先把数组转换为集合,然后再次使用扩展操作符把这个集合转换为数组:
let letters = [..."hello world"]
[...new Set(letters)] // ['h', 'e', 'l', 'o', ' ', 'w', 'r', 'd']
另一种创建数组的方式是使用Array()构造函数.有三种方式可以调用这个构造函数.
不传参数调用:
let a = new Array();
这样会创建一个没有元素的空数组,等价于数组字面量[].
传入一个数组参数,指定长度:
let a = new Array(10);
这样会创建一个指定长度的数组.如果提前知道需要多个数组元素,可以像这样调用Array()构造函数来预先为数组分配空间.注意,这时的数组中不会存储任何值,数组索引属性"0",“1”,等甚至都没有定义.
传入两个或更多数组元素,或传入一个非数值元素:
let a = new Array(5,4,3,2,1,"testing,testing")
这样调用的话,构造函数参数会成为新数组的元素.使用数组字面量永远比像这样使用Array()构造函数更简单.
在使用数值参数调用Array()构造函数时,这个参数指定的是数值长度.但在使用一个以上的数值参数时,这些参数则会成为新数组的元素.这意味着使用Array()构造函数无法创建只包含一个元素的数组.
在es6中,Array.of()函数可以解决这个问题.这是个工厂方法,可以使用其参数值(无论多少个)作为数组元素来创建并返回新数组:
Array.of()//[];返回没有参数的空数组
Array.of(10)// [10];可以创建只有一个数值元素的数组
Array.of(1,2,3) // [1,2,3]
Array.from()是es6新增的另一个工厂方法.这个方法期待一个可迭代的对象或类数组对象作为其第一个参数,并返回包含该对象元素的新数组.如果传入可迭代对象,Array.from(iterable)与使用扩展操作符[…iterable]一样.因此,他也是创建数组副本的一种简单方式:
let copy = Array.from(original);
Array.from()确实很重要,因为他定义了一种给类数组对象创建真正的数组副本的机制.类数组对象不是数组对象,但也有一个数值length属性,而且每个属性的键也都是整数.在客户端js
中,有些浏览器(dom)方法返回的值就是类数组对象,那么想这样先把他们转换为真正的数组便于后续的操作:
let dos = document.querySelectorAll("div")//NodeList
Array.from(dos)// Array(0)
Array.from()也接受可选的第二个参数,如果给第二个参数传入了一个函数,那么在构建新数组时,源对象的每个元素都会传入这个函数,这个函数的返回值将代替原始值称为新数组的元素(这一点和本章后面要介绍的数组的map()方法很像,但在构建数组期间执行映射的效率要高于先构建一个数组再把它映射为另一个新数组)
可以使用[]操作符访问数组元素,方括号左侧应该是一个对数组的引用,方括号内应该是一个具有非负整数值的表达式.这个语法可以读和写数组元素的值.因此,下面都是合法的js
语句:
let a = ['world']// 先创建一个包含一个元素的数组
let value = a[0];// 读取元素0
a[1] = 3.14;// 写入元素1
let i = 2;
a[i] = 3 ;// 写入元素2
a[i+1] = "hello"// 写入元素3
a[a[i]] = a[0];// 读取元素0和2,写入元素3
数组的特殊的地方在于,只要你使用小于2^32-1的非负数整数作为属性名,数组就会自动为你维护length属性的值.比如你在前面的例子中,我们先创建了一个只有一个元素的数组.而在给他的索引1,2,3赋值之后,数组的length属性也会相应改变.因此:
a.length // 4
记住,数组是一种特殊的对象,用于访问数组元素的方括号与用于访问对象属性的方括号是类似的,js
会将数组数组索引转换为字符串,及索引1会变成字符串 “1”,然后再将这个字符串作为属性名.这个数值到字符串的转换没什么特别的,使用普通对象也一样:
let o = {};// 创建一个普通对象
o[1] = "one"// 通过整数索引一个值
o["1"] // "one",数值和字符串属性名是同一个
明确区分数组索引和对象属性名是非常有帮助的.所有属性都是属性名,但只有介于0和2^32-2之前的整数属性名才是索引.所有数组都是对象,可以在数组上以任意名字创建属性.只不过,如果这个属性是数值索引,数组会有特殊的行为,即自动按需要更新其length属性.
注意,可以使用负数或非整数值来索引数组.此时,数值会转换为字符串,而这个字符串会作为属性名.因为这个名字是非负整数,所以会被当成常规的对象属性,而不是数组索引.
另外,如果你碰巧使用了非负整数的字符串来索引数组,那这个值会成为数组索引,而不是对象属性.同样,如果使用了与整数相等的浮点值1.1=>1也是如此:
a[-1.23] = true// 这样会创建一个属性"-1.23"
a["1000"] = 0;// 这是数组中第1001个元素
a[1.000] = 1;// 数组索引1,相当于a[1]=1
由于数组索引其实就是一种特殊的对象属性,所以js
数组没有所谓"越界"错误.查询任何对象中不存在的属性都不会导致错误,只会返回undefined.数组作为一种特殊对象也是如此:
let a = [true,false];// 数组的索引0和1元素
a[2] // undefined,这个索引没有元素
a[-1] // undefined.这个名字没有属性
稀疏数组就是其元素没从0开始索引的数组,正常情况下,数组的length属性表明数组中元素的个数,如果数组是稀疏的,则length属性的值会大于元素个数,可以使用Array()构造函数创建稀疏数组,或者直接给大于当前数组length的数组索引赋值.
let a = new Array(5)// 没有元素,但是a.length是5
a = [];// 创建了一个空数组,此时length=0
a[1000] = 0;// 赋值增加了一个元素,但length变成了1001
后面还会看到,使用delete操作符也可以创建稀疏数组.
租后稀疏的数组通常是以较稠密数组慢,但内存占用少的方式实现的,查询这种数组的元素与查询常规对象属性的时间相当.
注意,如果省略数组字面量中的一个值(想[1,3]这样重复逗号两次),也会得到稀疏数组,被省略的元素是不存在的.
let a1 = [,]// 这个数组没有元素,但length是1
let a2 = [undefined]// 这个数组有一个undefined元素
0 in a1 // false a1在索引0没有元素
0 in a2 // true :a2在索引0有undefined
理解稀疏数组是真正理解js
数组的重要一环,但在实践中,我们碰到的多数js
数组都不是稀疏的.如果真的碰到了稀疏数组,可以把稀疏数组当成包含undefined元素的非稀疏数组.
每个数组都有length属性,正是这个属性让数组有别于常规的js
对象,对于稠密数组(即非稀疏数组),length属性就是数组中元素的个数,这个值比数组的最高索引大1:
[].length //0 数组没有元素
["a","b","c"].length // 3最高索引2,length值为3
对于稀疏数组,length属性会大于元素个数,也可以说稀疏数组的length值一定大于数组中任何元素的索引.从另一个角度说,数组(无论稀疏与否)中任何元素的索引都不会大于或等于数组的length.为了维护这种不变式(invariant),数组有两个特殊行为.第一个前面已经提到了,即如果给一个索引i的数组元素赋值,而i大于或等于数组当前的length,则数组的length属性会被设置为i+1.
数组实现以维护长度不变式的第二个特殊行为,就是如果将length属性设置为一个小于其他当前值的非负整数n,则任何索引大于或等于n的数组元素都会从数组中被删除:
a=[1,2,3,4,5];// 先定义一个包含五个元素的数组
a.length = 3// a变成[1,2,3]
a.length = 0// 删除所有元素,a是[]
a.length = 5; // 长度是5,但是没有元素,类似new Array(5)
也可以把数组的length属性设置为一个大于其当前值的值,这样做并不会向数组中添加新元素,只会在数组末尾创建一个稀疏的区域.
我们已经看到过为数组添加元素的最简单方式了,就是给它的一个新索引赋值:
let a = [];//创建一个空数组
a[0] = "zero"// 添加一个元素
a[1] = "one"
也可以使用push()方法在数组末尾添加一个或多个元素:
let a =[]// 创建一个空数组
a.push("zero")// 在末尾添加一个值
a.push("one","two")// 再在末尾添加两个元素
像数组a中推入一个值等同于把这个值赋给a[a.length].要在数组开头插入值 ,可以使用unshift()方法(参见7.8节),这个方法将已有数组元素移动到更高索引位.与push相反的操作是pop()方法,他删除最后一个元素并返回该值,同时导致数组长度减一.类似的,shift()方法删除并返回数组的第一个元素,让数组长度减一并将所有元素移动到低一位的索引.7.8节有对这些方法的更多介绍,
可以使用delete操作符删除数组元素:
let a = [1,2,30]
delete a[2];
2 in a // false,数组索引2没有定义
a.length // 3 删除元素不影响数组长度
删除数组元素类似于(但不完全等同于)给元素赋undefined值,注意,对数组元素使用delete操作符不会修改length属性,也不会把高索引位的元素向下移动来填充被删除属性的空隙.从数组中删除元素后,数组会变稀疏.
如前所诉,把数组length属性设置成一个新长度值,也可以从数组末尾删除元素.
splice()是一个可以插入,删除或替换数组元素的通用方法.这个方法修改length属性并按照需要向更高或更低索引移动数组元素.详细介绍7.8节
到ES6位置,遍历一个数组(或任何可迭代对象)最简单方式是使用for/of循环5.4.5节介绍过:
let letters = [..."hello world"];// an array of letters
let string = ""
for(let letter of letters)[
string += letter
]
string // hello world
for/of循环使用的内置数组迭代器按照升序返回数组的元素.对于稀疏数组,这个循环没有特殊行为,反是不存在的元素都返回undefined.
如果要对数组使用for/of循环,并且想知道每个数组元素的索引,可以使用数组的entries()方法和结构赋值:
let everyother = ""
for(let [index,letter] of letters.entries()){
if(index%2===0)everyother+=letter;//偶数索引的字母
}
everyother // Hlowrd
另一种迭代数组的推荐方法是使用forEach().他并不是一种新的for循环,而是数组提供的一种用于自身迭代的函数式方法.因此需要给forEach()传入一个函数,然后forEach()会用数组的每个元素调用一次这个函数:
let uppercase = ""
letters.forEach(letter=>{
uppercase += letter.toUpperCase();
})
uppercase // "HELLO WORLD"
正如我们预期的,forEach()按顺序迭代数组,而且将索引作为第二个参数传给函数.与for/of循环不同,forEach()能够感知稀疏数组,不会对没有的元素数组调用函数.
7.8.1节会更详细地解释forEach()方法,该节也将介绍另外两个与数组迭代有关的方法:map()和filter().
当然,使用老式的for循环(参见5.4.3节)也可以遍历数组:
let vowels = "";
for(let i=0;i
在嵌套循环中,或其他性能攸关的场合,有时候会看到这种简单的数组迭代循环,但只会读取一次数组长度 ,而不是在每个迭代中都读取一次.下面展示的两种for循环形式都是比较推荐的:
// 把数组长度报错到局部变量中
for(let i=0,len =letters.length;i=0;i--){
//
}
这两个例子假定数组是稠密的,即所有元素都包含有效数据.如果不是这种情况,那应该在使用每个元素前进行测试.如果想跳过未定义或不存在的元素,可以这样写:
for(let i=0;i
js
并不支持真正的多维数组,但我们可以使用数组的数组来模拟.要访问数组的数组的值,使用两个[]即可.比如,假设变量matrix是一个数值数组的数组,则matrix[x]的每个元素都是一个数值数组,要访问这个数组中的某个数值,就要使用matrix[x][y]
这种形式.下面这个例子利用二维数组生成了乘法表:
// 创建一个多维数组
let table = new Array(10)// 表格的十行
for(let i =0;i
前几节主要介绍js
操作数组的基本语法.但是一般来说.还是Array类定义的方法用处最大,接下来几节将分别讨论这些方法.在学习这些方法的时候,要记住其中有的方法会修改调用他们的数组,而有些则不会.另外也有几个方法返回数组:有时候返回的这个数组是新数组,原始数值保持不变;而有时候原始数值会被修改,返回的是被修改后的数组引用.
接下来会集中介绍几个相关的数组方法:
下面几节也会介绍Array类的静态方法,以及拼接数组和把数组转换为字符串的方法.
本节介绍的方法用于迭代数组元素,他们会按照顺序把数组的每个元素传给我们提供的函数,可便于对数组进行迭代,映射,测试和归并.
在讲述这些方法前,有必要从整体上介绍一下这组方法.首先,所有这些方法都接受一个函数作为第一个参数,并且对素组的每个元素(或某些元素)都调用一次这个函数.如果数值是稀疏的,则不会对不存在的数组元素调用传入的这个函数.多数情况下,我们提供的这个函数被调用时,都会收到三个参数,分别是数值元素的值,数组元素的索引和数组本身.通常,我们只需要这几个参数中的第一个,可以忽略第二和第三个值.
接下来要介绍的多数迭代器都接受可选的第二个参数.如果指定这个参数,则第一个函数在被调用的时就好像他是第二个参数的方法一样.换句话说,我们传入的第二个参数会成为作为第一个参数传入的函数内部的this值.传入函数的返回值通常不重要,但不同的方法会以不同的方式处理这个返回值.本节介绍的所有方法都不会修改调用他们的数组(当然,传入的函数可能会修改这个数组)
所有这些方法在被调用的是第一个参数都是函数,因此在方法调用表达式中直接定义这个函数参数是很常见的,相对而言,使用在其他地方已经定义好的函数倒是不常见.箭头函数(8.1.3节)特别适合在这些方法中使用,接下来的例子中也会使用.,
forEach
forEach()方法迭代数组中的每个元素,并对每个元素都调用一次我们指定的函数.如前所述,传统forEach()方法的第一个参数是函数.forEach()在调用这个函数的时会给他传递三个参数:数值元素的值,数组元素的索引,和数组本身.如果只关心数组元素的值…可以把函数写成只接受一个参数,即忽略其他参数:
let data = [1,2,3,4,5],sum = 0;
// 计算数字之和
data.forEach(value=>{sum += value;})// sum =15
// 递增每个元素的值
data.forEach(function(v,i,a)=>{
a[i] = v + 1
}) // data == [2,3,4,5,6]
注意:forEach()并未提供一种提前终止迭代的方式.换句话说,在这里并没有与常规for循环中的break语句对等的机制.
map()
map()方法把调用他的数组的每个元素分别传给我们指定的函数,返回这个函数的返回值构成的数组一个新数组.例如:
let a = [1,2,3];
a.map(x=>x*x)// => [1,4,9] // 这个函数接受x并返回x*x
传给map()的函数传给forEach()的函数会以同样的方式被利用.但对于map()方法来说,我们传入的函数应该返回值.注意,map()返回一个新数组,并不修改调用他的数组,如果数组是稀疏的,则缺失元素不会调用我们的函数,但返回的数组也会与原始数组一样稀疏:长度相同,缺失的元素也相同.
filter()
filter()方法返回一个数组,该数组包含调用他的数组的子数组,传给这个方法的函数应该是个断言函数,即返回true或false的函数.这个函数与传给forEach()和map()的函数一样被调用.如果函数返回true或返回的值抓换为true,则传给这个函数的元素就是filter()最终返回的子数组的成员.例如:
let a = [1,2,3,4,5]
a.filter(x=>x<3)// [2,1]
// 索引为偶数 i从零开始 0 2 4
a.filter((x,i)=> i%2 ===0)// [1, 3, 5]
注意,filter()会跳过稀疏数组中确实的元素,他返回的数组始终是稠密的,因此可以使用filter()方法清理掉稀疏数组中的空隙
let dense = sparse.filter(()=>true)
如果即想清理空隙,又想删除值为undefined和null的元素,则可以这样写:
a = a.filter(x=>!==undefined && x !== null);
find()与findIndex()
find()和findIndex()方法和filter()类似,表现在他们都遍历数组,寻找断言函数返回真值的元素,但与filter()不同的是,这两个方法会在断言函数找到第一个元素时停止迭代,此时,find()返回匹配的元素,findIndex()返回匹配元素的索引.如果没有找到匹配的元素,则find()返回undefined,而findIndex()返回-1:
let a = [1,2,3,4,50]
a.findIndex(x=>x===3)// 2
a.findIndex(x=>x<0)// -1
a.find(x=>x%5==0)// 5
a.find(x=>x%7==0)// undefined:数组中没有7的倍数
every和some()
every()和some()方法是数组断言方法,即他们会对数组元素调用我们传入的断言函数,最后返回true或false
every()方法与数学上的"全称"量词"所有"类似,他在且只在断言函数对数组的所有元素都返回true时才返回true:
let a = [1,2,3,4,5];
a.every(x=>x<10)//true
a.every(x=>x%2===0)//false
some()方法类似于数学上的"存在"量词,只要数组元素中有一个让断言函数返回true他就返回true,但必须数组的所有元素对断言函数都返回false才返回false:
let a =[1,2,3,4,5]
a.some(x=>x%2===0)// true
a.some(isNaN)// false ,a没有非数值
注意,every()和some()都会在他们知道要返回什么值时停止迭代数组.
some()在断言函数第一次返回true时返回true,只有全部断言都返回false时才会遍历数组.
every()正好相反,他在断言第一次返回false的时候,返回false,只有全部断言都返回true的时才会遍历数组.同样也要注意,如果在空数组上调用他们,按照数学的传统,every()返回true!!!,some()返回false
reduce()与reduceRight()
reduce()和reduceRight()方法使用我们指定的函数归并数组元素,最终产生一个值,在函数编程中,归并是一个常见的操作,有时候也称为注入(inject)或折叠(fold).看例子更容易理解:
let a = [1,2,3,4,5];
a.reduce((new,old)=>new+old,0)// 15所有元素之和
a.reduce((x,y)=>x*y,1)//120所有值的乘积
a.reduce((x,y)=>(x>y)?x:y)// 找到最大值
reduce()接受两个参数.第一个参数是执行归并操作的函数.这个归并函数的任务就会把两个值归并或组合为一个值并返回这个值.在上面的例子中,归并函数通过把值相加,相乘和选择最大值来合并两个值.第二个参数是可选的,是传给归并函数的初始值.
在reduce()中使用的函数在与forEach()和map()中使用的函数不同.我们熟悉的值,索引和数组本身在这里作为第二,第三,第四个参数.
第一个参数是目前为止归并操作的累计结果.在第一次调用这个函数时,第一个参数作为reduce()的第二个参数的初始值.在后续调用中,第一个参数则是上一次调用这个函数的返回值.
在第一个例子中,初始调用归并函数传入的是0和1,归并函数将他们相加后返回1.然后再以参数1和2调用他并返回3.接着计算3+3=6,6+4=10,最后10+5=15.最终15成为reduce()的返回值.
有人可能注意到了,上面例子中第三次调用reduce()只传了一个参数,即并未指定初始值.在像这样不指定初始值调用时,reduce()会使用数组的第一个元素作为初始值.这意味着首次调用归并函数将以数组第一和第二个元素作为其第一和第二个参数.在求和与求积的例子中,也可以省略这个初始值参数.
如果不传递初始值参数,在空数组上调用reduce()会导致TypeError.如果调用他时只有一个值,比如用只包含一个元素的数组调用且不传初始值,或者用空数组调用但传了初始值,则reduce()直接返回这个值,不会调用归并函数.
reduceRight()与reduce()类似,只不过从高位索引向低位索引(从右到左)处理数组,而不是从低到高.如果归并操作具有从右到左的结合性,那可能要考虑使用reduceRight().比如:
// 计算 2^(3^4).求幂操作有从右往左的优先级
let a = [2,3,4];
a.reduceRight((acc,val)=>Math.pow(val,acc))//2.4178516392292583e+24
注意,无论reduce()还是reduceRight()都不接受用于指定归并函数this值的可选参数.他们用可选的初始值参数取代了这个值.如果需要把归并函数作为特定对象的方法调用,可以考虑8.7.5节介绍的Function.bind()方法
出于简单考虑,目前为止我们看到的例子都只涉及数值.但reduce()和reduceRight()并不是专门为数学计算而设计的.只要是能够把两个值(比如两个对象)组合成一个同类型的函数,都可以用作归并函数.另一方面,使用数组归并表达式的算法更容易复杂化,因而难以理解.此时可能使用常规循环逻辑处理数组反倒更容易阅读,编写和分析
在ES2019中,flat()方法用于常见并返回新数组,这个新数组包含与它包含于他调用flat()的数组相同的元素,只不过其中任何本身也是数组的元素会被"打平"填充到返回的数组中.例如:
[1,[2,3]].flat(); // 1,2,3
[1,[2,[3]]].flat();// 1,2,[3]
在不传参调用时,flat()会打平一级嵌套.原始数值中本身也是数组的元素会被打平.但打平后的元素如果还是数组则不会再打平.如果想打平更多层级,需要给flat()传一个数组参数:
let a = [1,[2,[3,[4]]]];
a.flat(1);// [1,2,[3,[4]]];
a.flat(2);// [1,2,3,[4]];
a.flat(3);// [1,2,3,4];
a.flat(4);// [1,2,3,4];
flatMap()方法与map()方法相似,只不过返回的数组会自动被打平,就像传给了flat()一样,换句话说,调用a.flatMap(f)等同于(但效率远高于)a.map(f).flat():
let phrases = ["hello world","the definitive guide"];
let words = phrases.flatMap(phrase=>phrase.split(" "));
words // ['hello', 'world', 'the', 'definitive', 'guide']
可以把flatMap()想象为一个通用版的map(),可以把输出数组中的一个元素映射为输出数组中的多个元素.特别地,flatMap()允许把输入元素映射为空数组,这样打平后并不会有元素出现在输出数组中
// 将非负数映射为他们的平方根
[-2,-1,1,2].flatMap(x=>x<0?[]:Math.sqrt(x))// [1,2**0.5]
concat()方法创建并返回一个新数组, 新数组包含调用concat()方法的数组的元素 ,以及传给concat()的参数.如果这些参数中有数值,则拼接的是他们的元素而非数组本身,但要注意,concat()不会递归打平数组的数组.concat()并不修改调用他的数组:
let a = [1,2,3]
a.concat(4,5);// 1,2,3,4,5
a.concat([4,5],[6,7])// 1,2,3,4,5,6,7,数组被打平了
a.concat(4,[5,[6,7]])// [1,2,3,4,5,[6,7]]
注意,concat()会创建调用他的数组的副本.很多情况下,这样做都是正确的,只不过操作代价有点大.如果你发现自己真正写类似a = a.concat(x)这样的代码,那应该考虑使用push()或splice()就地修改数组,就不要再创建新数组了
push()和pop()方法可以把数组作为栈操作,其中.push()方法用于在数组末尾添加一个或多个新数组,并返回数组的新长度.与concat()不同,push()不会打平数组参数.pop()恰好相反,用于删除最后面的元素,减少数组长度,并返回删除的值,注意,这两个方法都会就地修改数组.组合使用push()和pop()可以是使用js
数组实现先进后出的栈.例如:
let stack = []
stack.push(1,2)//[1,2]
stack.pop() 2//[1]
stack.push([3,4])// [1,[3,4]]
stack.pop() // [1]
push()方法不会打平传入的数组,如果想把一个数组中的所有元素都推送到另一个数组中,可以使用拓展操作符可以显式的打平它:
a.push(...values);///
unshift()和shift()方法与push()和pop()很类似,只不过他们是从数组开头而非末尾插入和删除元素,unshift()用于在数组开头添加一个元素或多个元素,已有元素的索引会响应向更高索引移动,并返回数组的新长度.shift()删除并返回数组的第一个元素,所以后续元素都会向下移动一个位置,以占据数组开头空出的位置.使用unshift()和shift()可以视为栈,但效率不如使用push()和pop(),因为每次在数组开头添加或删除元素都要向上或向下移动移动元素,不过,倒是可以使用push()在数组末尾添加元素,使用shift()在数组开头删除元素来实现队列:
let q = []
a.push(1,2)
q.shift() // 1
unshift()还有一个特性值得说一下.在给unshift()传多个参数时,这个参数会一次性插入数组,这意味着一次插入与多个插入之后的数组顺序不一样:
let a =[]
a.unshift(1) // 1
a.unshift(2) // 2,1
a = [] // []
a.unshift(1,2) // 1,2
数组定义了几个连续区域(或子数组,或数组"切片")的方法.接下来几节将介绍提取,替换,填充和复制切片的方法.
slice()
slice()返回一个数组的切片(slice)或者子数组.这个方法接受两个参数,分别用于指定要返回切片的起止位置.返回的数组中包含第一个参数指定的元素,以及所有后续元素,直到(但不包含) 第二个参数指定的元素.如果只指定一个参数,返回的数组将包含从起点开始直到数组末尾的所有元素.如果任何一个参数是负数,则这个值相对于数组长度指定数组元素.比如,参数-1指定数组的最后一个元素,参数-2指定倒数第二个元素.注意,slice()不会修改调用它的数组.下面看几个例子:
let a = [1,2,3,4,5];
a.slice(0,3);// [1,2,3]
a.slice(3)//[4,5]
a.slice(1,-1)//[2,3,4]
a.slice(-3,-2);//[3]// 不包含第二个参数指定的元素
splice
splice()是一个对数组进行插入和删除的通用方法.与slice()和concat()不同,splice()会修改调用他的数组!!! ,注意,splice()和slice()的名字非常相似,但执行的操作截然不同.
spllice()可以从数组中删除元素,可以向数组中插入新元素,也可以同时执行这两种操作.位于插入点或删除点之后的元素的索引会按照需要增大或减少,从而与数组剩余部分保持联系.
splice()的第一个参数指定插入或删除操作的起点位置.
第二个参数指定要从数组中删除(切割出来) 的元素个数(注意,这里是两个方法的另一个不同之处.slice()的第二个参数是终点.而splice()的第二个参数是长度).
若果省略第二个参数,从起点元素开始的所有数组元素都将被删除.splice()返回被删除元素的数组,如果没有删元素返回空数组!!! .例如:
let a = [1,2,3,4,5,6,7,8];
a.splice(4);// 返回[5,6,7,8],a现在是[1,2,3,4]
//[1,2,3,4]
a.splice(1,2);//返回[2,3] a现在是[1,4]
//[1,4]
a.splice(1,1) // 返回[4] a现在是[1]
splice()的前面两个参数指定要删除那些元素.这个两个参数后面还可以跟任意的多个参数,表示要在第一个参数指定的位置插入到数组中的元素.例如:
let a = [1,2,3,4,5];
a.splice(2,0,"a","b")// [];a现在是[1,2,"a","b",3,4,5]
// [1,2,"a","b",3,4,5]
a.splice(2,2,[1,2],3)// ["a","b"]; a现在是[1,2,"a","b",[1,2],3,3,4,5]
注意,与concat()不同,splice()本身插入数组本身,而不是数组的元素.
fill()
fill()方法将数组的元素或切片设置为指定的值,他会修改调用他的数组,也返回修改后的数组
let a = new Array(5);
a.fill(0) /// [0,0,0,0,0]
a.fill(9,1)//[0,9,9,9,9]
a.fill(8,2,-1)//[0,9,8,8,9]
fill()的第一个参数是要把数组元素设置成的值,可选的第二个参数指定起始索引,如果省略则从索引0开始填充.可选的第三个参数指定终止索引,到这个索引为止(但不包含)的数组元素会被填充.如果省略第三个参数,则从起始索引开始一直填充到数组末尾.与使用slice()一样,也可以传入负值相对于数组末尾指定的索引.
copyWithin
copyWithin()把数组切片复制到数组中的新位置.他会就地修改数组并返回修改后的数组,但不会改变数组的长度.
第一个参数指定要把第一个元素复制到的目的索引,
第二个参数指定要复制的第一个元素的索引.如果省略第二个参数,则默认值为0
第三个参数指定要复制的元素切片的终止索引.如果省略,则使用数组的长度.
起始索引到(但不包含)终止索引的元素会被赋值.与使用slice()一样,也可以传入负值相对于数组末尾指定索引:
let a = [1,2,3,4,5]
a.copyWithin(1)// [1,1,2,3,4]:把数组元素复制到索引1及之后[1,2,3,4,5]=> [1,[1,2,3,4,5]]=>[1,1,2,3,4]
a.copyWithin(2,3,5) // [1,1,3,4,4]:把最后两个元素复制到索引2
[11234]=>[34]=>[11344]
a.copyWihhin(0,-2) // [4,4,3,4,4] // 负偏移量也可以
[11344]=>[44]=>[44344]
copyWithin()本意是作为一个高性能方法,尤其是对定型数组11.2节特别有用,他模仿的是c标准库中的memmove()函数,注意,.即使来源和目标区域有重叠,复制也是真确的.
数组实现与字符串的同名方法类似的indexOf(),lastIndexOf()和includes()方法,此外还有sort()和reverse()方法用于对数组元素重新排列.下面介绍这些方法:
indexOf()和lastIndexOf()
indexOf()和lastIndexOf()从数组中搜索指定的值并返回第一个找到的元素的索引,如果没有找到则返回-1.indexOf()从前向后(或从头到尾)搜索数组,而lastIndexOf()从后向前搜索数组:
let a = [0,1,2,1,0]
a.indexOf(1)// 1 a[1] 1
a.lastIndexOf(1)// 3 a[3] 1
a.indexOf(3)// -1 没有元素的值是3
indexOf()和lastIndexOf()使用===操作符比较他们的参数和数组元素.如果数组包含对象而非原始值,这些方法检查两个引用是否引用同一个对象.如果想查找对象的内容,可以使用find()方法并传入自定义的断言函数.
indexOf()和lastIndexOf()都接受第二个可选的参数,指定从哪个位置开始搜索.如果省略这个参数,indexOf()会从头开始搜索,lastIndexOf()会从尾开始搜索.第二个参数可以是负值,相对于末尾偏移,与slice()方法一样.比如,-1指定数组的最后一个元素.
下面这个函数从指定的数组中搜索指定的值,并返回所有匹配元素的索引.这个例子演示了indexOf()的第二个参数可以用来找到除了第一个之外的匹配值.
// 从数组a中找到所有值x,返回匹配索引的数组
function findAll(a,x){
let results = [];// 要返回的索引数组
len = a.length;
pos = 0;// 搜索的起始位置
while(pos
注意,字符串也有indexOf()和lastIndexOf()方法,跟着两个数组方法类似,区别在于第二个参数如果是负值会被当成0
includes
ES2016的includes()方法接受一个参数,如果数组包含该值则返回true,否则返回false.他并不是告诉你值的索引,只告诉你是否存在.includes()方法实际上是测试数组的成员是否属于某个集合.不过要注意,数组并非集合的有效表达方法,如果元素数量庞大,应该选择真正的Set对象(11.1.1节)
includes()方法与indexOf()方法有一个重要区别.indexOf()使用与===操作符同样的算法测试相等性,而该相等算法将非数值的值看成与其他值都不一样,包括与其自身也不一样.includes()使用稍微不同的相等测试,认为NaN与自身相等,这意味着indexOf()无法检测数组中的NaN值,但includes()可以:
let a = [1,true,3,NaN];
a.includes(true) //true
a.includes(NaN) // true
a.indexOf(NaN) // -1 indexOf()无法找到NaN
sort
sort()对数组元素就地排序并返回排序后的数组.在不传参调用时,sort()按字母顺序对数组元素排序(如有必要,临时把他们转换为字符串再比较):
let a = ["banana","cherry","apple"]
a.sort() // ["apple","banana","cherry"]
如果数组包含未定义的元素,他们会被排到数组末尾.
要对数组元素执行非字母顺序的排序,必须给sort()传一个比较函数作为参数.这个函数决定他的两个参数哪一个在排序后数组中应该出现在前面.
如果第一个参数应该出现在第二个参数前面,比较函数应该返回一个小于0的数值(升序).
如果第一个参数应该出现在第二个参数后面,比较函数应该返回一个大于0的数值(降序).
如果两个值相等(也就是他们的熟悉不重要),则比较函数应该返回0.
因此,要对数组元素按照数值而非字母顺序排序,应该这样做:
let a = [33,4,1111,222];
a.sort();// a == [1111,222,33,4]// 字母顺序
a.sort(function(a,b){//
return a-b;
})// [4,33,222,1111]
a.sort((a,b)=>b-a)// a == [1111,222,33,4],相反数值顺序
再来看一个排序数组元素的例子.如果相对字符串数组做不区分大小写的字母序排序,传入的比较函数应该(使用toLowerCase()方法),将其两个参数都转换为小写,然后在比较:
let a = ["ant","Bug","cat","Dog"];
a.sort();//['Bug', 'Dog', 'ant', 'cat']
a.sort(function (s,t){
let a = s.toLowerCase(); //
let b = t.toLowercase(); ///
if(ab)return 1;
return 0;
});// a == ['ant', 'Bug', 'cat', 'Dog']
reverse()
reverse()方法翻转数组元素的顺序,并返回反序后的数组.这个反序是就地反序,换句话说,不会用重新排序后的元素创建新数组,而是直接对已经存在的数组重新排序:
let a = [1,2,3]
a.reverse() // a == [3,2,1]
Array类定义了3个把数组转换为字符串的方法,通常可以用在记录日志或错误消息的时候(如果想把数组的文本内容保存起来以备后用,可以使用js
ON.stringify()[参见6.8节]方法对数组执行序列化,而不是使用这里介绍的方法).
join()方法把数组的所有元素转换为字符串,然后把他们拼接起来并返回结果字符串.可以指定一个可选的字符串参数,用于分割结果字符串中的元素.如果不指定分隔符,则默认使用逗号:
let a =[1,2,3];// 不会改变原来的数组
a.join() "1,2,3"
a.join(" ") // "1 2 3"
a.join("")// "123"
let b = new Array(10);//
b.join("-") // "----------------"包含9个连续字符串
join()方法执行的是String.split()方法的泛型操作,后者通过把字符串分割为多个片段来创建数组.
与任何js
对象一样,数组也有toString()方法,对于数组而言,这个方法的逻辑与没有参数的join()方法一样:
[1,2,3].toString() // 1,2,3
["a","b","c"].toString() // a,b,c
[1,[2,"c"]].toString() // 1,2,c
注意,输出中不包括方括号或数组值的定界符.
toLocaleString()是toString()的本地化版本,他调用toLocaleString()方法将每个数组元素转换为字符串,然后再使用(实现定义的)当分隔符字符串来拼接结果字符串.
除了前面介绍的数组方法,Array类也定义了3个静态方法,可以通过Array构造函数而非数组调用.Array.of()和Array.from()是创建新数组的工厂方法,分别在7.1.4和7.1.5节介绍过了.
另一个静态组函数是Array.isArray(),用于确定一个未知值是不是数组
Array.isArray([])// true
Array.isAaray({})// false
如前所见,js
数组具有一些其他对象不必备的特殊特性.
这些特性让js
数组与常规对象有了明显区别,但是,这些特性并非定义数组的本质特性,事实上,只要对象有一个数值属性length,而且有响相应的非负整数属性,那就完全可以视同为数组.
实践中,我们偶尔会遇到"类数组"对象,虽然不能直接在他们上面调用数组方法或期待length属性的特殊行为,但仍然可以通过写给真正数组的代码来遍历他们.说到底,就是因为很多数组既适用于真正的数组,也适用于类数组对象.特别是在将数组视为只读或者至少不会修改数组长度情况下,就更是这样了.
下面的代码会为一个常规对象添加属性,让它成为一个类数组对象,然后再遍历得到的伪数组的"元素":
let a = {}// 创建一个常规的空对象
// 添加属性让他变成"类数组"对象
let i = 0 ;
while(i<10){
a[i] = i*i;
i++;
}
a.length = i;
// 像遍历真正的数组一样遍历这个对象
let total = 0;
for(let j=0;j
在客户端js
中,很多操作HTML文档的方法(比如document.querySelectorAll())都返回类数组对象.下面的函数可以用来测试对象是不是类数组对象:
// 确定o是不是类数组对象
// 字符串和函数有数值length属性,但是通过typeof可以排除.在客户端中
// DOM文本结点有数值length属性,可能需要加上o.nodeType!==3测试来排除
function isArrayLike(o){
// 类数组的前提是一个对象
if(o &&// o不是null,undefined等假值
typeof o==="object" && // o是对象
Number.isFinite(o.length) && // o.length是有限数值
o.length>=0 && // o.length是非负数值
Number.isInteger(o.length)&& //o.length是整数
o.length< (2^32-1) // o.length < 2^32-1
){
return true;
}
return false;//否则不是类数组对象
}
下一节会介绍字符串的行为也是数组类似.但无论如何,上面对类数组对象的测试对字符串会返回false,字符串最好还是作为字符串而非数值来处理.
多数js
数组方法有意地设计成了泛型方法,因此除了真正的数组,同样也可以用于类数组对象.但由于类数组对象不会继承Array.prototype,所以无法直接在他们上面调用数组方法,为此,可以使用Function.call() 方法(8.7.4节)来调用:
let a = {"0":"a","1":"b","2":"c",length:3};// 类数组对象
Array.prototype.join.call(a,"+")// a+b+c
Array.prototype.map.call(a,x=>x.toUpperCase())// ["A","B","C"]
Array.prototype.slice.call(a,0) // 真正的数组副本
Array.from(a) // ["a","b","c"]//更容易的数组复制
倒数第二行代码在类数组对象上调用了Array的slice()方法,把该对象的元素复制到一个真正的数组对象中,在很多遗留代码中,这都是很常见的代码,但现在使用Array.from()更容易
js
字符串的行为类似于UTF-16 Unicode字符串的只读数组.除了使用charAt()方法访问个别字符,还可以使用方括号语法:
let s = "test";
s.charAr(0) // "t"
s[1] //e
当然对于字符串来说,typeof操作符任然返回"string",把字符串传给Array.isArray()方法仍然返回false
可以通过索引访问字符串的好处 ,简单来说就是可以用方括号代替charAt()调用,这样更简洁也更容易理解,可能效率也更高,不过,字符串与数组的行为类似也意味着我们可以对字符串使用泛型的字符串方法,比如;
Array.prototype.join.call("Javascript"," ") //"J A V A....."
一定要记住,字符串是不可修改的值,因此在把他们当成数组来使用时,他们是只读数组,像push(),sort(),reverse()和splice()这些就地修改数组的数组方法,对字符串都不起作用.但尝试用数组方法修改字符串并不会导致错误,只会静默失败.