Array
这章主要解释数组,数组是JavaScript中的一类基础数据类型,在很多语言里也一样。数组是一个有顺序的数据集合。其中的每一个数据被叫做一个元素,每个元素都有一个数字下标,这被成为索引,JavaScript数组不限制数据类型,也就是说里面的元素类型可以是任意的。数组元素甚至可以是一个对象或者其他数组,你可以构建出极其复杂的数据结构,比如一个存储对象的数组,或者存储数组的数组。JavaScript数组的索引从0开始,同时其范围可以涵盖32bit,也就是说,你可以存储不超过4294967294个元素。同时你不需要指定数组大小,它会根据你对数组的操作对数组大小进行修改。JavaScript数组可以是断断续续的,也就是说你允许在元素间存在空隙,并不需要每一个位置都填入元素。每一个数组都会有一个length属性,对于非间断的数组而言,length代表其元素的个数。然而对于间断的数组而言,length的大小取决于数组中元素索引的最大值。
数组在JavaScript中是一种特殊的对象。数组索引也仅仅只是它的属性名而已,只是恰好用整数表示。访问数组元素会比访问普通对象的值快得多,后面会详细讲到。
数组继承了Array.prototype,里面包含很多数组操作方法,在7.8中会讲到。其中大部分方法都是通用的,这意味着他们不仅仅适用于数组,还适用于其他像数组的对象。这部分内容在7.9中会提到。最后一点,JavaScript String 看起来像字符数组,这里会在7.10里讲解到。
ES6介绍了一种新型的数组被称为类型数组,不同于传统的数组类型,类型数组拥有一个固定的长度,同时限制其元素的类型,他们在访问比特层面具有很好的表现,11.2中会讲到。
7.1 Creating Arrays
创建一个数组有多种方法,接下来这些个小章节会详细讨论这几种情况。
7.1.1 The Array Literals
创建一个数组最简单的方法是通过数组字面量,它仅仅只是被[]框住的以逗号分隔的元素序列,举个例子。
let empty = [];
let primes = [2,3];
let misc = [1.1,true,"a",];
数组字面量中的元素不一定就非得是常量,他们也可以是任何表达式:
let base = 1024;
let table = [base,base+1];
数组字面量可以包含其他对象字面量或者数组字面量。
let b = [[1,{x:1, y:2}]];
如果一个数组字面量表达式里包含连续的逗号,之间也不存在值,这种方式创建的数组会成为间断数组,在这些间断位置上的元素会被忽略,如果你尝试去获取他们,你将得到undefined。
let count = [1,3];
let undefs = [,,];
字面量表达式允许存在一种曼生逗号,在这种条件下[,,]的数组长度为2,而非3.
7.1.2 The Spread Operator
在ES6以后,你可以通过使用“展开操作符” ... ,以字面量的形式实现将一个数组元素合并进另一个数组。
let a = [1,2,3];
let b = [0,...a,4];
...会展开数组a然后,a其中的所有元素会嵌入到下方这个数组字面量表达式中。...a会被替代为a数组中全体元素,排列在数组b中特定的索引范围内。(要注意一点,尽管我们将...称为张开操作符,但它并不是传统意义上的操作符,因为它仅仅只能用在数组字面量中,以及函数引用值)
在新建一个数组的备份时,使用展开操作符是一个非常方便的方法:
let original = [1,2,3];
let copy = [...original];
copy[0] = 0; // 修改备份并不会影响到原数组
original[0] // => 1
展开操作符适用于任何可以被枚举的对象(可枚举的对象是指被for/of枚举的东西,我们第一次看到他们在5.4.4,12章还会经常看到),String是可枚举的,所以可以通过展开符将任何string转变成一个字符数组。
let digits = [..."3792"];
digits // => ['3','7','9','2']
Set 对象(11.1.1)是可枚举的,所以去除一个数组中重复的元素,一个非常简单的方式是将其转变为一个set,然后立刻通过展开符将其转回一个array.
let letter = [..."hello world"];
[...new Set(letters)] // => ['h','e','l','o','w','r','d']
7.1.3
另一种方式创建对象是通过Array()构造方法,调用这个构造方法一共有三种不同的方式。
- 无参数调用:
let a = new Array();
这个方法会创建一个空对象,相当于字面量方式创建[] - 调用时传入一个数字参数,指明其数组长度。
let a = new Array(10)
这种方式指定了新建数组的长度。可以用于提前分配一个数组,适用于你提前获知该数组长度的情况下。但需要注意的是,这个数组并没有存储任何的值。
- 明确声明两个或多个元素,抑或单个非数字类型的元素。
let a = new Array(5,4,"testing, testing");
在这种情况下构造方法内值会成为该数组的元素,使用一个数组字面量来创建其实更简单。
7.1.4 Array.of()
当Array()构造函数以一个数字类型参调用时,他会将这个数字作为数组的长度,而当传入的参数大于1时,所有的参数会作为数组的元素,这里就有个问题,Array()并不能创建一个仅有一位数字元素的数组。
ES6解决了这个问题,现在你可以通过Array.of()工厂方法来创建对象,不管多少参数,统统被视作新数组的元素。
Array.of() //=>[];返回一个不含任何参数的空数组
Array.of(10) //=>[10];创建一个包含10这个元素的数组
Array.of(1,2,3) //=>[1,2,3]
7.1.5 Array.from()
Array.from() 也是在ES6中提出的另一种工厂方法,该方法第一个参数应为一个可枚举或者array-like对象。然后他会返回一个包含这个参数中所有元素或属性的数组。Array.from()的作用和展开符[...iterable]一样,作为一个复制数组最简单的方式。
let copy = Array.from(original)
Array.from()的重要之处在于,它能将一个Array-like对象复制成一个货真价实的Array数组,Array-like对象是一个非数组对象,但却有着一个数字类型的属性length,同时所有属性的属性名刚好是整数。在JavaScript客户端,某些浏览器的方法是array-like。如果在使用他们之前,你能够将其转变为真数组,可能会让你用起来更顺手。
let truearray = Array.from(arraylike);
Array.from()可以接受两个参数,如果你将函数作为其第二个参数。在所有元素被复制完成以后,每一个元素都会被传入该函数中,函数的返回值将作为新数组对应索引的值。(这和Array的map()方法很相似,但是from()方法会更加高效,当这个数组被新建。。。)
7.2 Reading and writing Array Elements
你可以通过[]操作符来访问一个数组中的元素,数组引用在左边,右边的[]中可以放置任意的表达式,表达式最终可以表达为非负数。你可以通过这种方法来读取或者写入数据。因此,下方的表达式都是合法的JavaScript表达式。
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的元素
数组的不同之处在于,当你的属性名为非负整数,同时在0-2ˆ32 - 1,数组会自动维护length属性的值,在前面这个例子中,我们创建了一个单元素数组,当我们给索引1,2,和3赋值时,Length属性的值会自动改变。
a.length // =>4
你需要记住数组属于一种特殊的对象。[]在访问数组元素时就像你通过它访问对象一样,JavaScript将数字1强转为字符串'1',然后将1作为对象的属性名去访问它的值。整个过程没有什么特殊的操作,你可以使用一个普通对象来实现这一过程
let o = {} //创建一个空对象
o[] = "one"; //使用整数作为属性名
o["1"] // => "one"; 数字和字符串的作用是一样的。
搞清楚数组索引和对象属性名很有用,所有的索引都属于属性名,但是这些属性名却是整数,范围在0-2ˆ32之间。所有的数组都是对象,也就是说你可以使用任何值作为属性名使用。如果你使用索引作为其属性名,JavaScript有着特殊的机制来更新length属性。
你是可以使用负数来作为数组属性名的,当你这么做的时候,这个负数会转为字符串,同时将其作为数组的属性名。然而因为它并不属于正整数,因此会被当做普通对象属性来处理。而非数组索引。同时,如果你通过一个正整数字符串来访问数组,它表现得就像一个对象,而不是一个对象属性。这种同样的情况还发生在浮点数上,如果浮点数恰好等于一个正整数。
a[-1.23] = true; //创建了一个名字叫“-1.23”的属性。
a["1000"] = 0; //给第10001个元素赋值。
a[1.000] = 1; //索引1,和a[1] = 1;一样。
数组索引仅仅只是一种特殊的属性名意味着,JavaScript数组不可能出现任何越界错误。当你尝试去找到一个不存在的属性,你不会得到任何报错信息。而是返回undefined,就像对对象的操作那样。
let a = [true, false];
a[2] //=>undefined:该索引无值。
a[-1] //=>undefined:没有这个名字的属性。
7.3 Sparse Arrays
间断数组是指数组拥有从0开始的非连续索引,一般而言,length属性代表着数组元素的个数,可一个间断数组的length会比该数组中的属性值更大。你可以通过Array()或者将值赋给数组中一个比最大索引值还大得多的索引。
let a = new Array(5); // 没有数组,但是a.length的值为5.
a = []; //创建一个空数组,length为0.
a[1000] = 0; //仅仅将一个值赋给了数组,但是length的大小是1001.
后面我们还能用delete操作符实现间断数组。
过于松散的数组通常 are implemented in a slow, more memory-efficient way then dense arrays are.同时在这种数组中查找元素所需时间几乎和普通的对象差不多。
需要注意的是,当你在通过数组字面量方式创建数组的时候,在两个元素之家使用了重复的逗号,这个数组会变成间断数组。中间忽略掉的元素值是undefined。
let a1 = [,]; //数组为空,但length为1。
let a2 = [undefined]; //拥有一个undefined元素的数组。
0 in a1 //=>false: a1该索引位置无元素。
0 in a2 //=>true: a2在该索引位置值为undefined。
理解间断数组对于了解JavaScript数组的本质十分重要。实际上,在实战中你很少会碰到间断数组。就算你碰到了,你大概率还是像对待非间断数组那样,只不过获取到很多undefined元素。
7.4 Array Length
每一个数组都有一个Length属性,同时这也是数组区别于其他普通对象的一个很重要的特征。对于连续数组而言,Length代表该数组中所有元素的个数。它的大小只比数组中最大索引值大1.
[].length //=> 0: the array has no elements.
["a","b","c"].length // => 3: 最大索引值为2,length为3.
对于一个间断数组,Length的值将比数组中元素个数大得多。总的来说,我只能保证length的值将会比任何元素的索引值大,换个说法,这个数组中将不会有一个数组的index比它大或者说等于它。为了保证这点,数组存在两个特俗的性质。第一种方式已经讲过了,如果你将一个超过或者等于当前数组length的索引赋值了,length的值会自动+1。
第二个性质是,如果你将一个数组如果你设置的length值比当前数组的length值更小,每一个索引大于或者等于这个值的元素将被移除。
a = [1,2,3,4,5] //以五个初始元素创建的数组
a.length = 3 //a现在变成了[1,2,3]
a.length = 0 //所有元素都会被删掉,a现在变成了[]
a.length = 5 //length现在等于5,但是数组里没有任何元素,就像刚通过new Array()方式创建的那样。
你仍可给length属性赋予任何比它大的值,这并不会导致数组被添加进入任何元素,而是仅仅在数组后半部分创建了一个间断区域。
7.5 Adding and Deleting Array Elements.
我们已经用过一种很简单的方式向数组中添加一个元素,直接将一个值赋给一个新索引。
let a = []; //创建了一个空数组。
a[0] = "zero"; //And add elements to it。
a[1] = "one";
你也可以选择使用push()方法将元素添加到数组的尾部。
let a = []; //初始化一个空数组
a.push("zero"); //添加一个元素到数组尾部。a = ["zero"]
a.push("one", "two"); //同时添加多个元素。a = ["zero","one","two"]
将一个元素放入数组相当于分配a[a.length]操作,你也可以通过unshift()方法插入一个元素到数组的最前面,这会导致数组中原有属性索引变大。pop()方法是一个和push()方法完全相反的方法,他会一处数组中最后一个元素。同时返回它的值。同理shift()是一个和unshift()完全相反的方法。它会删除数组中第一个元素,同时返回它的值。并且将数组中所有元素的索引向更小的值移动一位。7.8章节中有更详细的描述。
你可以使用 delete操作符 删除任意的数组元素,就像你对对象属性做的那样。
let a = [1,2,3];
delete a[2]; //a在2位置的元素为空
2 in a // => false: 索引2上的值为空。
a.length //=> 3: 删除元素不会影响其length大小
删除一个元素和给该索引值赋予undefined相似。需要注意的是,删除数组元素并不会改变length的大小,也不会导致数组高位索引向下移动来填补该位置的空值。如果你删除了一个连续数组的元素,该数组会变为间断数组。
前面讲过一种方法,你可以通过设置length的方法,变相从数组尾部移除元素。
最后介绍以下splice()方法,它是通用的插入,删除或者替换数组元素方法。他会修改length属性,同时移动所有索引的值,以填补数组中的空隙。在7.8章节中会讨论到。
7.6 Iterating Arrays
在ES6中,最简单的遍历数组的方法是使用 for/of 这个知识已经在5.5.5章节中讲过了。
let letters = [..."Hello World"];
let stirng = "";
for(let letter of letters){
string += letter;
}
string // => "Hello world"; 重组了这个字符串。
数组内置的迭代器将返回数组内每个元素,按照递增的顺序。但却对间断数组没有任何处理,而仅仅只是返回间断点位置的空值(undefined)
如果你想对一个数组使用for/of循环,同时获知index的值。你可以通过数组的entries()方法,与拆卸赋值使用时候就像下面这样。
let everyother = "";
for(let [index, letter] of letters.entries()){
if(index/2 === 0) everyother += letter; //在当前索引位置的字符。
}
另一个很好的便利数组方式是通过forEach()方法,这是一个新型for循环,也就是提供了一种函数式写法来访问数组迭代器。你可以传送一个函数给forEach()方法,同时forEach()将会在每一轮迭代唤醒你的函数一次。
let uppercase = "";
letters.forEach(letter => {
uppercase += letter.toUpperCase();
})
uppercase //"HELLO WORLD"
就像你想的那样,forEach()按照index的大小遍历数组中的元素,实际上他还会传给你指定位置的索引值,这个值会赋给函数的第二个参数,有时候会有点用。但不同于 for/of 循环,forEach()知晓间断数组,而且会跳过这些位置的元素。
7.8.1提供了关于forEach()方法的更多信息。这章同样概述了其他一些相关的方法,例如map()和filter(),他们都以某种特殊的形式表现了数组迭代器。
你同样可以通过老实循环来遍历所有元素。5.5.3章节中的for loop
let vowels = "";
for(let i = 0; i < letters.length; i++){ //取得数组中的每个元素
let letter = letters[i] // 获取相应位置的index元素
if(/[aeiou]/.test(letter)){ // 调用正则表达式的test方法
vowels += letter; // 当其他属于一个vowel
}
}
vowel //=>"eoo"
在嵌套循环中,或者其他注重性能的场景,你可能会发现这种基础的迭代循环被使用。因此length将只会被调用一次,而不会在每一次的迭代器中使用。下面两种for循环都是合法的表达式。虽然看起来不寻常,但配合着现代JavaScript解释器,其实并不清楚他们有没有任何性能影响。
//将length属性存储到一个本地变量中
for(let i = 0, len = letters.length; i < len; i++){
//循环体保持相同。
}
//反向迭代数组。
for(let i = letters.length; i >= 0; i__){
//循环体内容相同。
}
这些例子假设该数组为连续数组,同时所有元素都是合法的。如果事情没有像你预想中那样发生,你就应该在使用他们之前验证以下。如果你想跳过undefined和不存在的元素,你可以这么写。
for(let i = 0; i < a.length; i++){
if(a[i] === undefined) continue; //跳过undefined和空值。
// 循环体在这。
}
7.7 Multidimensional Arrays
JavaScript并不会真正支持多维数组。但是你可以通过创建包含数组元素的数组来近似模拟出一个。访问一个二维数组的方式是通过array[][]的方式来访问。举个例子,比如说现在有一个变量matrix是一个二维数组,matrix中每一个元素都是一个数字数组。如果你尝试去访问其中的某个数值,你可以这样写:matrix[x][y].这里是一个具体的例子,通过二维数组表示一个乘法表。
//创建一个多维数组
let table = new Array(10); //数组中一共有10行。
for(let i = 0; i < table.length; i++){
table[i] = new Array(10) // 每一行有十列。
}
// 初始化该数组。
for(let row = 0; row < table.length; row++){
for(let col = 0; col < table[row].length; col ++){
table[row][col] = rowcol;
}
}
//通过多维数组来计算57
table[5][7]
7.8 Array Methods
前面的内容主要在讲JavaScript数组的基本语法,然而数组中定义的方法才是使得其强大的原因。下面的内容重点讲解了这些方法。在你理解这些方法的时候,一定要记得区分好,一类是会对数组产生作用,一类却不会。很多的数组方法都会返回一个数组,有时候这是一个新数组,原数组没有受到任何影响,有时候,一些方法会修改源数组,同时返回源数组的引用。
下面这些小章节概括了很多与之相关的数组方法。
- 迭代器方法迭代出数组中的每个元素,通常带着数组中每个元素被唤起多次。
- 栈和队列方法,增加或者删除元素从数组的起点和终点。
- subarray methods 用于提取,删除,插入,填补,赋值连续的大型数组区域。
- 搜索和排序方法用于定位以及对数组进行排序。
下面这些小章节中同样概述了一些静态方法和很多乱七八糟的方法,可以用于连接,或者将数组转变成字符串。
7.8.1 Array Iterator methods
本章中的这些方法迭代所有的数组元素,通过将数组元素作为参数传给回调函数。可用于迭代,过滤,测试,map,缩小。
在详细解释之前,我们应该对这些方法有一种全局的观念。所有的这些方法都将接受一个函数作为其第一个参数,同时在迭代每一个元素时被唤起一次。如果这个数组属于间断数组,在间断点上会跳过。在大多数情况下,你的函数会带着三个参数被唤醒,数组元素值,数组元素索引,数组的引用。一般情况下你只需要第一个参数,而第二个第三个参数很少用到。
在这章中讨论到的大多数迭代器方法可以接受第二个参数,如果声明了,第一个参数(函数)会作为第二个参数的一个属性值。也就是说。第二个参数会变成第一个函数的this引用。函数的返回值通常很重要。但是不同的方法有着不同的方式去处理这点。这些方法通常不会对调用者产生影响,当然你传入的函数是可以修改源数组的。
这些个方法里,每一个都接受一个函数作为其第一个参数,同时你可以直接将函数定义在方法体内,使用箭头函数是一种非常常见的方法。
forEach()
forEach()方法遍历数组中所有元素,每迭代一次就唤起一次函数。前面以及解释过,你将一个函数传入方法中充当第一个元素,forEach()会唤醒这个函数,同时将三个参数传入其中。元素值,元素索引,数组引用。如果你仅仅只是关心数组内的值,那就在写函数的时候,只提供第一个参数,其他参数忽略掉就行了。
let data = [1,2,3,4,5],sum = 0;
//计算数值之和
data.forEach(value =>{ sum += value;});
//现在递增数组中所有元素。
data.forEach(function(v, i, a){ a[i] = v + 1}); //data == [2,3,4,5,6]
要注意的是,forEach()并没有提供中断枚举的功能,也就是说,在枚举完所有元素之前,并没有像普通loop中的循环中的break表达式。
map()
Map()方法将所有的数组元素一个个传给你声明的函数中,并将回调函数的返回值组合成数组作为该方法的返回值。举个例子:
let a = [1,2,3];
a.map(x => xx) // => [1,4,9]: 该函数取得输入的x后返回xx
map()方法的运作方式与forEach()基本类似,不同之处在于map()传入的回调函数应该返回一个值。还有就是它会返回一个新数组,并且源数组不会被改变。在间断点上你的函数并不会被唤起,但是新数组中原先的间断点会依旧存在。新数组中依然会保留原有的length与原有的间断点。
filter()
Filter()方法会返回一组元素。你提供的函数应该属于一个predicate,一个仅返回true或者false的函数。或者说可以转为true或false的值。predicate就像forEach()和map()那样被调用。如果返回值为true或者被转为了true。这时候这个元素会被分配到一个小组里,最后变成返回的新数组中的一部分。举个例子。
let a = [5,4,3,2,1];
a.filter(x => x < 3) // => [2,1]; 小于三的数值。
a.filter((x,i) => i%2 === 0); // => [5,3,1];所有的奇数
需要注意的是,filter()会跳过所有间断点,同时其返回值将永远是连续数组。当你有消除间断点的需求时,可以通过这个方法来实现。
let dense = sparse.filter(() => true);
同时如果你同时有消除间断点和移除所有undefined以及null元素的需求,你可以同样使用filter,就像这样:
a = a.filter(x => x !== undefined && x !== null);
find()和findIndex()
find()和findIndex()方法和filter()很像,遍历所有元素并找出那些让你的predicate函数返回truthy值的元素,但不一样的点在于,这两个方法会在遇到第一个让predicate函数返回true的时候,终止遍历。这时候,find()返回该元素,findIndex()返回该元素的索引。如果没有找到这个元素,find()返回undefined,findIndex()返回-1:
let a = [1,2,3,4,5]
a.findIndex(x => x === 3) // => 2; 数值3位于索引2
a.findIndex(x => x <0 ) // => -1; 索引不存在负数
a.find(x => x % 5 === 0 ) // => 5; 这是5的倍数
a.find(x => x % 7 === 0) // => undefined; 没有找到七的倍数。
every()和some()
every()和some()方法属于数组predicates:他们通常依赖于你声明的predicate function。返回值为true或false。
every()方法就像数学中的"for all"数学符号。它仅仅在你数组中所有的元素都返回true时才会返回true。
let a = [1,2,3,4,5]
a.every(x => x < 10) // => true: 所有的值都小于10。
a.every(x => x % 2 === 0) //=> false: 并不是说所有的值都是偶数。
some()方法相当于数学符号存在:只要predicate function返回过true,some()就会返回true。且只有当所有经过predicted function都返回false时,它才会返回false。
let a = [1,2,3,4,5]
a.some(x => x % 2 === 0) // => true; a数组内存在偶数。
a.some(isNaN) // => // => false; a数组没有非数字元素。
要注意无论是every()或者是some(),它们在知晓应该返回什么布尔值的时候,会立即退出。例如every()存在元素的predicate function返回返回false的时候,立即就返回false了,而不会接着执行后面未枚举的元素,some()也一样,只要遇到有predicate function 返回了true,会立刻返回true,而不会等到最后枚举完所有元素再返回。同样记住这点,根据规定,对于一个空数组,every()返回true,some()返回false。
reduce() 和 reduceRight()
reduce()和reduceRight()方法将数组中的元素组合起来。通过你提供的函数,产出一个数值。这在函数式编程中是一种常见的操作。同时遵循两个概念"inject"和"fold".来个例子会更好理解。
let a = [1,2,3,4,5];
a.reduce((x,y) => x+y, 0) // =>15; 所有元素值之和。
a.reduce((x,y) => x*y, 1) // =>120; 所有元素之积。
a.reduce((x,y) => (x > y) ? x : y) // => 5; 数组中的最大值。
reduce()可以插入两个参数,第一个是执行reduction操作的函数。reduction 函数的任务是通过某种操作将两个值组合或者化为一个一个值,然后返回它。在前面那个例子中,reduction 函数通过相加,相乘,排序将两个值组合到一起。第二个参数作为一个初始值传给该reduction函数。
reduce()和forEach()的回调函数的使用方式完全不一样,元素值,元素索引,数组引用将作为第二,第三,第四个参数。第一个参数是目前被合并后的值。在第一次唤起回调函数时,函数的第一个参数是你传入reduce()方法的第二个参数。在随后的唤起中,它的值是前一轮唤起返回的值。在第一个例子中,reduction 函数第一次以0和1为参数被调用了,随后返回1,第二次以1和2唤起,返回3,以此类推,最终获得15,作为reduce()的返回值。
你可能注意到在第三次调用reduce()的时候,仅仅只传入了一个函数,并没有声明初始值。而第一次的函数回调时,reduction function会是数组的第一个值,而第二个参数会是数组的第二个值。在和与积的例子里,我们完全可以忽略传入初始值。
以一个空数组去调用reduce()方法,且不提供初始值,将会导致一个TypeError错误。如果你以单值方式调用reduce(),可以是只含有一个值的数组,不提供初始值的情况;也可以是空数组,但是提供了一个初始值。这两种情况下,数组会直接返回该值,而不会唤起reduction 函数。
reduceRight()的用法和reduce()基本一样,但是它从数组的高索引位置开始工作,而不是从低到高。你可能会有需要从右向左计算的需求,举例子:
//计算2(34). 指数计算从右边开始计算。
let a =[2,3,4];
a.reduceRight((acc,val) => Math.pow(val,acc)) // => 2.4178516392292583e+24
值得注意的是,reduce()和reduceRight()可以接受一个额外的参数,该参数会作为reduction函数内部的this。看看Function.bind()8.7.5,如果你需要某个reduction函数作为某个对象的方法被调用。
上述例子中到目前为止仅涉及到数字类型,但这仅仅只是为了方便理解。reduce(),reduceRight()绝非仅仅只适用于数学计算。任何可以实现合二为一的函数,且合成后的值与源值类型相同,都可以作为reduction函数。换句话讲,使用reductions来实现算法可能会将你的代码变得复杂且难以理解。同时你可能会发现通过普通的循环来实现,会更加容易阅读,书写与思考。
7.8.2 Flattening arrays with flat() and flatMap()
在ES2019中,flat()方法创建并且返回一个与调用它的数组相同的数组。除了一些本身就是数组的元素,其他普通元素都将被展开到返回的数组中。举个例子:
[1,[2,3]].flat() // => [1,2,3]
[1,[2,[3]]].flat() // => [1,2,[3]]
当不传入任何参数去调用这个方法的时候,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()的用法差不多,除了一点,返回值中的数组将自动展开到新数组中。其实也就相当于array.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()和flat()的组合,它使得数组中的每个元素都可以拆解成多个元素,而如何拆解将由你传入的函数决定。在特定情况下,flatMap()可以将输入的元素输出为空数组,于是在展开后的数组中什么也没有:
//将所有非负数转为平方根。
[-2, -1, 1, 2].flatMap(x => x < 0 ? [] : Math.sqrt(x)) //=> [1, 2**0.5]; 0.5表示指数,twice*将表示指数表达式。
7.8.3 Adding arrays with 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,4,5,6,7]; 数组被展开。
a.concat(4,[5,[6,7]]) // => [1,2,3,4,5,[6,7]]; 多层数组保留。
a; // => [1,2,3]; 源数组无变化。
concat()会生成一份源数组的备份。在许情况下,这么做是正确的,但这个操作非常的expensive.如果你发现你写的代码就像a = a.concat(x),你应该考虑用push()或者splice()而不是新建一个数组。
7.8.4 Stacks and /Queues with push(), pop(),shift(),and unshift()
push()和pop()方法允许你像操作一个栈那样操作一个数组。push()方法将一个或多个元素添加到一个数组的尾部,同时返回数组的length值。但和concat()有所不同,push()不会展开你提供的数组。pop()方法的操作相反,它删除数组中最后一个元素,同时改变length值,这两个方法会修改数组原值。
通过组合这两个方法,你可以实现一个first-in, last-out stack。举个例子。
let stack = []; //stack == []
stack.push(1,2); //stack == [1,2]
stack.pop(); //stack == [1]; return 2
stack.push(3); //stack == [1,3]
stack.pop(); //stack == [1]; return 3
stack.push([4,5]) // stack == [1,[4,5]]
stack.pop() //stack == [1]; return [4,5]
stack.pop() //stack == []; return 1
push()方法并不会展开你提供的函数,但如果你想要将一个数组元素全部展开到另一个数组中,你可以使用展开操作符。
a.push(...values);
unshift()和shift()方法的作用和push(),pop()相似,不同之处在于,unshift()将元素添加到数组首部,同时将所有元素向更高位置的索引移动,最后返回数组的length值。shift()删除数中第一个元素,并将其后所有元素的索引向更低位置移动一位,填补空缺的位置,最后返回被删除数组的值。你可以使用shift(),unshift()来实现一个stack,但这没有push(),pop()用起来高效。因为在你每次新增或者删除数组元素之后,你需要左右移动数组的index。你也可以通过组合push()和shift()来实现一个queen(First in, First out)。
let q = [];
q.push(1,2); //q == []
q.shift(); //q == [2]; return 1
q.push(3); //q == [2,3]; return 2
q.shift(); //q == [3]; return 2
q.shift(); //q == []; return 3
这里还有一个关于unshift()的特征值得引起注意,那就是当你一次unshift多个元素进数组中时,它们会被一次性全部插入进去,这就导致插入的数组元素顺序与一个一个元素插入的时候是完全颠倒的。
let a = []; //a == []
a.unshift(1) //a == [1]
a.unshift(2) //a == [2,1]
a = []; //a == []
a.unshift(1,2); // a == [1,2]
7.8.5 Subarrays with slice(),splice(),fill(),and copyWithin()
数组定义了很多方法专门对数组的连续区域,部分,或者裁切。下面这部分内容针对对数组的提取,替换,填补,以及复制。
slice()
slice()方法会返回一个数组的片段,或者说部分数组,它的两个参数指明了这个片段的起点和重点。这个片段会包含这个第一个参数声明的元素,和随后能够取得的元素,但不包含第二个参数所声明的元素。相当于数学中的[a,b).如果你仅仅只传入一个元素,那么,这个片段会包含第一个元素以及随后的所有元素。如果某个参数为负数,它对应的索引位置会进行换算,-1代表数组中索引值最大的元素,以此类推,-2代表length-2,slice不会篡改它的调用者。下面是一些简单的例子:
let a = [1,2,3,4,5];
a.slice(0,3); //Returns [1,2,3]
a.slice(3); //Return [4,5]
a.slice(1,-1); //Return [2,3,4]
a.slice(-3,-2); //Return [3]
splice()
splice()是一个综合性大的方法,可以用于插入或者删除一个数组的元素。和slice(),concat(),splice()的操作会影响其调用者。splice()和slice()拥有相同的名字,但却是两个截然不同的操作。
splice()可以删除,也可以插入元素,或者同时做这两件事。数组进行插入或者移除操作后,相应的元素index会进行修改以保持数组元素的连续性。方法中的第一个参数确定了在进行插入,移除操作时,开始的位置。第二个参数指明了操作量。(注意一点,这里的参数和slice不一样,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]
a.splice(1,2); // => [2,3]; a现在的值为[1,4]
a.splice(1,1); // => [4]; a现在的值为[1]
开头的两个参数指明了删除数组中哪些元素,而两个参数之后的元素则会成为数组中被插入的元素。而插入的位置会和第一个参数有关。举个例子:
let a = [1,2,3,4,5];
a.splice(2,0,'a','b'); // => [1,2,'a','b',3,4,5];
a.splice(2,2,[1,2],3); // => [1,2,[1,2],3,3,4,5];
还是要提醒以下,和concat()不一样,splice()是直接将参数插入数组,如果传入的参数是一个数组,那么这个数组会直接插入,而非里面的值。
fill()
fill()方法重置一个数组中的所有元素,或一部分,到一个特定的值。它会修改数组本身,同时返回修改后的数组:
let a = new Array(5); // 创建一个长度为5的空数组。
a.fill(0); // =>[0,0,0,0,0];将所有的元素设置为5。
a.fill(9,1); // => [0,9,9,9,9];将索引为1以及其后的所有元素设置为9。
a.fill(8,2,-1) // => [0,9,8,8,9];将[2,3]区域内的元素重置为8
fill()的第一个参数指明将数组重置为何值,第二个参数指明重置操作的起点,且可以忽略掉,第三个参数指明了操作结束的位置,注意不包含此位置。你可以通过负数来指明数组反方向的相对位置,就像你在使用slice那样。
copyWithin()
7.8.6 Array Searching and sorting Methods
Arrays 实现了indexOf(),lastIndexOf(),和includes()方法,这些方法和string中的方法很相似。同时还存在sort(),reverse()方法用于改变数组的顺序。这些方法在下面这些小结中被讲到。
indexOf() and lastIndexOf()
indexOf()和lastIndexOf()通过提供的特殊值在数组中搜寻一个特定的元素。同时返回第一次找到这个元素的位置。如果没有找到则返回-1。indexOf()从数组索引的开始搜寻到结束的位置。lastIndexOf()从数组结束的位置开始向前找。
let a = [0,1,2,1,0];
a.indexOf(1); // => 1: a[1] is 1
a.lastIndexOf(1); // => 3: a[3] is 1
a.indexOf(3); // => -1:no element has value 3.
indexOf()和lastIndexOf()通过“===”来比较你数组中的属性。如果你的数组中存在元素是对象,便会通过比较对象引用来确定是否相等。如果你想通过对象内容来确定两对象是否相同,改用find方法,通过你自己的predicate函数来查找。
indexOf()和lastInndexOf()还存在第二个参数,借此指明应该从数组的哪个位置开始,默认为0和length-1.负数索引也是可以被使用的,同时被当作一个offset从数组的尾部。因为它们和slice()方法有关,-1将表示数组中的最后一个元素。
下面的程序实现了查找一个数组中的指定元素,同时返回存在该元素的所有索引的集合。这很好地解释了如何通过第二个参数来找到比第一个匹配值更远位置的索引。
找到所有的匹配值,并且返回由这些索引所组成的数组。
function findall(a, x){
let result = [], len = a.length, pos = 0;
while(pos < len){ //当存在元素给我去找的时。
pos = a.indexOf(x, pos); //找
if(pos == -1) break; // 如果没有找到,退出!!!
results.push(pos); //否则,存起来。
pos++; // 开始下一次的搜索。
}
return results; // 返回索引数组。
}
String中的indexOf()和lastIndexOf()方法和它的工作原理差不多,但区别在于,如果你给string的这些方法第二个参数传负数,会等效于0。
includes
在ES2016中有一个includes()方法,只需要提供一个参数,也仅仅只会返回一个布尔值。在数组包含你提供的参数时,返回true。它不会指明这个值在数组中的位置,仅仅只告诉你有没有。It is efficient representation test for arrays。注意,arrays并不是一个高效的set表示方式。如果你要操作的数据量不是一个两个,你应该使用一个真正的Set对象。
includes()方法相较于indexOf()有一个很不同的点。indexOf()执行时的算法和'==='一样。而这个比较算法会将not-a-number的值看作是完全不同的值,包括他自己。而includes()使用一个稍微有点不同的比较算法来考虑这个问题,所以它可以查询数组中是不是存在NaN值。所以,结论就是indexOf不可以查询到数组中是不是存在NaN值,但是includes()可以。
let a = [1,true,3,NaN];
a.includes(true) // => true
a.includes(2) // => false
a.includes(NaN) // => true
a.indexOf(NaN) // => -1; indexOf不能找到NaN
sort()
sort()对数组元素进行排序,同时返回排序后的值。当你对其进行无参调用的时候,会通过字母顺序进行排序。(如果有需要的话,暂时将其转换成String类型进行比较。):
let a = ["banana","cherry","apple"];
a.sort(); // a == ["apple","banana","cherry"];
如果数组中含有undefined,这些元素会被放置在数组的尾部。
为了能够将数组以其他方式进行排序,你可以传入一个特定的函数进行比较。这个函数决定了,函数的两个参数,哪一个应该排在数组的前面。如果第一个参数在第二个参数前,函数就应该返回一个负数,如果这个参数应该在数组的后方,则函数应该返回一个正数。同时,如果这两个参数旗鼓相当,那就返回0。举个例子,如果你想以数值大小来排序,而非字符大小,你可以这么做。
let a = [33,4,1111,222];
a.sort(); // a == [1111,222,33,4]
a.sort(function(a,b){ //传入比较函数
return a - b; // 根据实际情况返回<0,>0,=0。
});
a.sort((a,b) => b-a); // a == [1111,222,33,4]; 倒序
另一个例子中,你可能想通过将字符串全部转成小写体,使得比较时不被字符的大小写影响,
let a = ["ant", "Bug", "cat", "Dog"];
a.sort(); // a == ["Bug", "Dog", "ant", "cat"]; 大小写敏感。
a.sort(function(s,t){
let a = s.toLowerCase();
let b = t.toLowerCase();
if(a < b) return -1;
if(a > b) return 1;
return 0;
}) // a == ["ant", "Bug", "cat", "Dog"]; 大小写不敏感。
reverse()
reverse()方法将数组的顺序颠倒,同时返回这个反转后的数组。它在原有数组的基础上操作,换句话说,它并不会创建一个新数组来存储这些重新排列后的数组,而是在已有数组的基础上进行重新排序。
let a = [1,2,3];
a.reverse();
7.8.7 Array to String Conversions.
Array类提供了三个方法用于将数组转变成字符串。这些方法通常在你想要打印日志或者错误信息时候会用得到。(如果你是想将其存储为文本状态,方便在以后使用它,也可以通过JSON.stringify()来序列化该数组)。
join()方法将数组中所有元素转成string,并把他们连接起来,最后将其返回。你可以通过一个可选的参数来指定字符之间的分隔方式,如果没有声明这个参数,‘,’会作为分隔符。
let a = [1,2,3];
a.join() // => "1,2,3"
a.join(" ") // => "1 2 3"
a.join("") // => "123"
let b = new Array(10); //一个长度为10的空数组。
b.join("-") // => "---------":
join()方法是String.split()方法相反方法。它通过拆分一个String将其变成数组。
Arrays,和所有的JavaScript对象一样,也拥有toString()方法。对于一个数组,调用这个方法就像无参调用join()方法那样。
[1,2,3].toString() // => "1,2,3"
["a","b","c"].toString // => "a,b,c"
[1,[2,"c"]].toString // => "1,2,c"
但是还是要提一嘴,结果中不会包含方括号或者其他分隔符。
toLocalString() is the localized version of toString(). It converts each array element to a string by calling the toLocaleString() method of the element, and then it concatenates the resulting strings using a locale-specific(and implementation-defined) separator string.
Static Array Functions
关于对数组方法的补充,Array 类同样定义了三个静态方法,你可以直接通过数组的构造方法来调用。Array.of()和Array.from()是构造新数组的工厂方法。在7.1.4和7.1.5被描述过。
另一个静态方法是Array.isArray(),这个方法可以查明一个值是否是一个数组:
Array.isArray([]) // => true
Array.isArray({}) // => false
7.9 Array-Like Objects
我们已经了解到,Javascript 数组存在一些特俗的性质,是普通对象没有的:
- length属性会被自动更新。
- 减小length值会导致数组被修改。
- 继承了Array.prototype的很多属性
- Array.isArray()可以识别数组。
这些属性是使得JavaScript数组区别于其他普通对象的特征。但它们并不是定义一个数组所必需的特征。把任何存在一个整数型的length属性与一系列以非负整数为属性名的属性成为数组是可以理解的。
这些“array-like”的对象在实操中偶尔会出现。虽然说你不能直接在他们身上唤起哪些针对Array数组的方法,或者期待着它的length也存在特殊的性质。然而你任旧可以像普通的一个数组那样去遍历它。结果是很多针对数组的算法针对它们而言完全通用。尤其在当你的算法只是将这个数组视为可读的数组时,或者length属性根本没有机会被修改时。
下面的代码操作着一个普通数字,将元素添加到里面使其表现得就像一个数组那样。然后遍历这个生成后的假数组。
let a = {}; // 以一个普通的空数组开始
//将元素添加到里面去,使它变成一个"array-like"
let i = 0;
while(i < 10){
a[i] = i*i;
i++;
}
a.length = i;//现在就像对待一个数组那样去遍历它。
let total = 0;
for(let j = 0; j < a.length; j++){
total += a[j];
}
在客户端的JavaScript中,很多用于操作HTML documents的方法(比如说document.querySelectorAll())都是基于"array-like"对象的。这里有一个函数,可能你会用它去测试一个对象是不是一个“array-like”对象。
//确定o是不是一个"array-like" 对象。
//Strings 和 functions存在length属性,但会被typeof test排除。在client-side JavaScript中,DOM text 节点也有一个数字类型的length属性。也可能需要通过o.nodeType !== 3被排除掉。
function isArrayLike(o){
if(o && // o一定非空,undefined,etc.
typeof o === "object" && //o 一定是一个对象
Number.isFinite(o.length) && // o.length是一个finite数字
o.length >= 0 && // o.length是一个非负数
Number.isInteger(o.length) && //o.length 是一个整数
o.length < 4294967295 //o.length一定小于2^32-1
){
return true; //o是一个array-like
}else{
return false; // o不是一个array-like
}
}
后面会有一章我们学到String的表现和Array很相似。然而,这类测试通常不会将其视为array-like,它们最好作为string来操作,而不是数组。
大多数JavaScript数组方法被故意设计成通用的,所以它们可以直接应用到Array-like对象身上还有真正的数组。由于array-like对象并不直接从Array.prototype中继承方法,你不可以直接调用数组的方法。而是间接地通过Function.call方法,however:
let a = {"0":"a", "1":"b", "2":"c", length:3}; //一个array-like数组。
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) // => ["a","b","c"]: 真数组拷贝。
Array.prototype.from(a) // => ["a","b","c"]: 复制数组的简单方式。
倒数第二行的代码调用了数组的slice()方法作用于一个array-like对象,以确保将其转成一个真数组。这是一个很常用的技巧,而且存在于许多过时的代码中,但现在通过Array.from()会更加简单。
7.10 Strings as Arrays.
Javascript strings 表现得就像一个仅读的UTF-16方式编码的字符集合。除了charAt()方法,你还可以通过方括号的语法来获取其中的单个字符:
let s = "test";
s.charAt(0) // => "t"
s[1] // => "e"
typeof操作符任然返回“string”,同时Array.isArray()方法也返回false。
通过索引去访问的好处在于,我们不需要再通过charAt()方法去访问了,相比较这种方式会更加具有可读性,也可能会更高效。Strings表现得就像一个数组,于是数组的通用方法同样适用于String,举个例子:
Array.prototype.join.call("JavaScript"," ") // => "J a v a S c r i p t"
但请记住,Strings是一个不可修改的值。所以当它们被当作数组来使用的时候,它们属于仅可读的数组。数组中的其他方法比如说push(),sort(),reverse(),还有splice()这些会直接修改数组本身的方法并不会生效。当你尝试做这些事情的时候,并不会报错,仅仅是失败而已。
Summary
这章深入讲解了JavaScript中的数组,包括难以理解的间断数组与array-like对象。
- 数组字面量通过一对方括号来表示一个数组。
- 单个的数组元素通过方括号包裹索引值来访问
- for/of 循环和 ... 展开操作符是ES6中引入的,并且对于枚举数组元素非常有用。
- Array类定义了很多方法用于操作数组,同时你应该保证对这些api烂熟于心。