函数那一部分为了尽量的避免踩坑,花了不少时间、精力和篇幅,也从阮老师(阮一峰)的博客找了不少资料过来,同时也推荐一下他的网站:
闲话不多说,接着来看看数组。
1 概念
数组(array)是按次序排列的一组数据,每个数据的位置都有对应的编号——从0开始编号,整个数组用方括号标示,如下就是一个成员为a、b、c的数组:
let arr = ['a', 'b', 'c'];
上面三个数组成员的位置,分别是0、1、2:
通过数组编号(位置),也可以在定义之后赋值:
由上面的例子,可以看出同一个数组可以支持许多不同类型的数据;事实上,任何类型的数据,都可以放入数组:
如上,数组arr的最后三个成员依次是对象、数组和函数。
其中,如果数组的成员还是数组,就形成了多维数组:
本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object:
数组的特殊性其实就是它的键名——按次序排列的一组整数:0,1,2,3……
我们可以通过Object.keys方法查看对象的键名,用它就可以看到数组的键名,即内部编号:
由于数组成员的键名是固定的(默认总是0、1、2...),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串;同样,在赋值时也是成立的——一个值总是先转成字符串,再作为键名进行赋值。如下例:
上面使用了7.00作为键名,但由于自动转换为字符串时是7,故而通过数字7可以读取对应值。
在讲述对象的章节当中,提及过两种读取对象成员的方法:object.key和object[key];如果键名为数值,object.key是不可以使用的。
所以数组作为一种特殊的对象,也同样是只能使用方括号的:
2 length属性
数组的length属性,会返回数组的成员数量:
JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个( )个,也就是说length属性的最大值就是 4294967295。
只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1。
上面代码表示,数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。
length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值;如下例,将数组myArr的length设置为3,后面所有的值都被删除了:
所以将数组的length属性设为0,就是一个很有效的清空数组的方法:
反过来,如果把length人为设置一个大于当前成员个数的值,数组成员的数量会增加到这个值,所有新增的位置都是空位:
如果人为设置length为不合法的值,JavaScript就会报错:
有一种比较特殊的情况需要特别留意:因为数组本质上也是对象,所以可以为数组添加属性,但不影响length的结果:
上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。
如果数组的键名是添加超出范围的数值,该键名会自动转为字符串;此时,由于键名转为字符串,length属性也不会发生变化,但读取时,数字键名会默认转为字符串,所以还是能够读取到的:
3 相关运算符
3.1 in运算符
检查某个键名是否存在的运算符in,适用于对象,也适用于数组。
上面代码表明,数组存在键名为2的键。由于键名都是字符串,所以数值2会自动转成字符串。
注意,如果数组的某个位置是空位,in运算符返回false。
3.2 扩展运算符
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列:
该运算符主要用于函数调用。
上面代码中,array.push(...items)和add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。
扩展运算符与正常的函数参数可以结合使用,非常灵活。
function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);
扩展运算符后面还可以放置表达式。
const arr = [
...(x > 0 ? ['a'] : []),
'b',
];
请注意:如果扩展运算符后面是一个空数组,则不产生任何效果;并且只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
上面后面的三个例子,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。
- 替代函数的 apply 方法
由于扩展运算符可以展开数组,所以不再需要apply方法来将数组转为函数的参数了。
// ES 5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES 6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。
// ES 5 的写法
Math.max.apply(null, [14, 3, 77])
// ES 6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。
另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。
// ES 5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES 6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
上面代码的 ES 5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。
下面是另外一个例子。
// ES 5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES 6
new Date(...[2015, 1, 1]);
- 扩展运算符的应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。
上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。
ES 5 只能用变通方法来复制数组。
上面代码中,a1会返回原数组的克隆,再修改a3就不会对a1产生影响。
扩展运算符提供了复制数组的简便写法。
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
上面的两种写法,a2都是a1的克隆。
(2)合并数组
扩展运算符提供了数组合并的新写法。
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES 5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES 6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
不过,这两种方法都是浅拷贝,使用的时候需要注意。
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
a3[0] === a1[0] // true
a4[0] === a1[0] // true
上面代码中,a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。
(3)与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。
// ES 5
a = list[0], rest = list.slice(1)
// ES 6
[a, ...rest] = list
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
(4)字符串
扩展运算符还可以将字符串转为真正的数组。
上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符,如下:
上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。
凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。
4 数组遍历
由于数组就是对象,所以其实for...in也是可以遍历数组的:
但是,for...in不仅会遍历数组所有的数字键,还会遍历非数字键:
因此通常不建议使用for...in来遍历数组,可以考虑使用for或者while循环来代替:
const a = [1, 2, 3];
// for循环
for(let i = 0; i < a.length; i++) {
console.log(a[i]);
}
// while循环
let i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}
let l = a.length;
while (l--) {
console.log(a[l]);
}
上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。
不过在实际编程中,推荐直接使用数组的forEach方法,举例如下:
或者for...of:
5 数组的空位
数组的空位指数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。
上面代码中,Array(3)返回一个具有 3 个空位的数组。
注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。
上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。
ES 5 对空位的处理,已经很不一致了,大多数情况下会忽略空位(以下方法将在后面讲到JavaScript标准库的时候做出讲解)。
- forEach(), filter(), reduce(), every() 和some()都会跳过空位。
- map()会跳过空位,但会保留这个值
- join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。
ES 6 则是明确将空位转为undefined。
Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。
扩展运算符(...)也会将空位转为undefined。
copyWithin()会连空位一起拷贝。
fill()会将空位视为正常的数组位置。
for...of循环也会遍历空位。
上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。
entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
// keys()
[...[,'a'].keys()] // [0,1]
// values()
[...[,'a'].values()] // [undefined,"a"]
// find()
[,'a'].find(x => true) // undefined
// findIndex()
[,'a'].findIndex(x => true) // 0
由于空位的处理规则非常不统一,所以建议避免出现空位。
6 类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object):
上面代码中,对象obj就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。
“类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化:
上面代码为对象obj添加了一个数字键,但是length属性没变。这就说明了obj不是数组。
典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集(将在后面专门介绍),还有字符串:
上面三个例子,它们都不是数组(instanceof运算符返回false),但是看上去都非常像数组。
数组的slice方法可以将“类似数组的对象”变成真正的数组:
除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面:
上面代码中,arrayLike代表一个类似数组的对象,本来是不可以使用数组的forEach()方法的,但是通过call(),可以把forEach()嫁接到arrayLike上面调用。
下面的例子就是通过这种方法,在arguments对象上面调用forEach方法:
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同于 for 循环
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call遍历:
不过一定要注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法:
评论
发表评论