JavaScript权威指南--第七章 数组(学习总结)

199 阅读19分钟

第七章 数组

7.1 创建数组的方式

  1. 数组直接量

  2. 构造函数new Array()

    有三种方式调用构造函数。

    // 创建一个没有任何元素的空数组,等同于数组直接量[]
    const arr1 = new Array(); 
    
    // 指定了数组的长度,但数组中没有存储值,甚至数组的索引属性等还未定义
    const arr2 = new Array(10);
    
    // 显式指定数组的一个非数组元素或2个及以上的数组元素
    const arr3 = new Array(5,4,3,2,1,"testing"); 
    

数组是对象的特殊形式。使用方括号访问数组元素就像用方括号访问对象的属性一样。JavaScript 将数组的数字索引值转换成字符串,然后将其当做属性名来使用。例如索引值1转换成‘1’。

所有的数组索引都是对象的属性名,但只有在0-2^32-2之间的整数属性名才是索引(因为数组的最大可能的索引值时2^32-2,最大长度为2^32-1)

数组是对象的一种形式,如果我们使用了除0-2^32-2之间的整数属性名,例如负整数或非整数,这个数值会转换为字符串来作为数组的属性名,而不是索引;而如果使用了0-2^32-2之间的整数字符串,则会将此整数字符串转换为数字,作为数组的索引。

// 创建一个数组
const arr = [];
// 给数组对象设置了一个'-1'的属性名,-1将转换为字符串'-1'
arr[-1] = 1000;
// 和arr[1]相同
arr[1.000] = 'hello';
// 和arr[1000]相同
arr['1000'] = 'world';

实际上,数组索引是对象属性的一种特殊类型。

7.2 稀疏数组

稀疏数组指的是包含从0开始的不连续索引的数组。可以使用构造函数Array()或简单指定数组的索引值大于当前的数组长度来创建稀疏数组。

// 不是稀疏数组 类似[undefined,undefined,undefined]
const arr1 = [,,,]; // 这里有3个逗号,最后一个逗号后面是没有值的
console.log(1 in arr1); // false 在书中写的是输出为true,但实践之后发现实际为false
console.log(arr1.length); // 3

const arr2 = [undefined,undefined,undefined];
console.log(1 in arr2); // true 与上面的情况做对比,说明两者还是有区别的

// 是稀疏数组
const arr3 = [1];
arr3[1000] = 2; // 稀疏数组
console.log(1 in arr3); // false

// 是稀疏数组,之前已经提到过,指定了数组的长度,但数组中没有存储值,甚至数组的索引属性等还未定义
const arr4 = new Array(1000);
console.log(1 in arr5); // false

7.3 数组长度

数组的索引是从0开始的,当数组不是稀疏数组时,数组的长度length属性的值大小为最大索引+1。

如果为一个数组元素赋值,它的索引index大于或等于现有数组的长度时,length属性值将被设置为index+1。

如果设置length属性值为一个小于当前长度的非负整数n时,当前数组中索引值大于或者等于n的元素将被删除。

const arr1 = [0,1,2,3,4];
arr1.length = 3; // 删除下标大于等于3的数组元素 输出[0,1,2]
arr1.length = 0; // 删除所有元素,变成[]空数组
arr1.length = 5; // 长度为5,稀疏数组,和new Array(5)一样

如果设置length属性值为大于当前长度的正整数时,不会向数组中添加新元素,只在数组尾部创建一个空的区域。

在ES5中,可以使用Object.defineProperty()使数组的length属性设置为只读。

const arr2 = [0,1,2];
Object.defineProperty(arr2,"length",{
    writable: false
});// 将数组arr2的length属性设置为只读
arr2.length = 1000; // 想设置数组的长度为1000,但是设置不了
console.log(arr2.length); // 3

7.4 数组元素的添加和删除

7.4.1 数组元素的添加

  1. 为新索引赋值
  2. push()方法在数组尾部添加1个以上的元素,在尾部添加一个元素时,与arr[arr.length]赋值是一样的
  3. unshift()方法在数组的首部插入一个元素

7.4.2 数组元素的删除

  1. delete运算符不会修改数组的长度,它只是将该索引位置的值设置为空白(不是undefined,这是有区别的,如果是undefined,那就不是稀疏数组了),将数组变成了稀疏数组。
  2. pop()方法,每执行一次,可使数组长度-1,并且返回被删除元素的值。
  3. shift()方法,与unshift()方法相反,从首部删除一个元素,并返回删除的元素值,其他所有元素的索引都要相应的-1。
  4. splice()方法可以做到插入、删除或者替换数组元素。
const arr1 = [0,1,2];
delete arr1[1]; // 数组长这样了[0,,2],不是这样的[0,undefined,2],看下面
console.log(1 in arr1); // false 说明[0,,2]
console.log(arr1.length); // 3
console.log(arr1); // [0,,2]

const arr2 = [0,1,2,3,4,5];
console.log(arr2.pop()); // 返回被删除的元素值5
console.log(arr2.shift()); // 返回被删除的元素值0

7.5 数组遍历

// 排除null、undefined、0、false这些转化为false的值(或者已经是false的值)和不存在的元素
const arr = [0,false,undefined,null,,1];
for(let i = 0; i < arr.length; i++) {
    if (!arr[i]) continue;
    console.log(arr[i]);
} // 只会打印1

// 只想跳过undefined和不存在的元素
for(let i = 0; i < arr.length; i++) {
    if(arr[i] === undefined) continue;
    console.log(arr[i]);
} // 依次打印0、false、null、1

// 只想跳过不存在的元素,仍要处理undefined元素
for(let i = 0; i < arr.length; i++) {
    if(!(i in arr)) continue;
    console.log(arr[i]);
} // 依次打印0、false、undefined、null、1

// 由于不存在的元素没有索引,for/in可以过滤不存在的元素,但是for/in能够枚举继承的属性名
for (let index in arr) {
	console.log(arr[index]);
}// 依次打印0、false、undefined、null、1

// 使用for/in循环数组时,需要用额外的方法来过滤继承的属性名(例如添加到Array.prototype中的方法)
for (let index in arr) {
    if (!arr.hasOwnPrototype(index)) continue; //跳过继承属性
	console.log(arr[index]);
}
for (let index in arr) {
    // Math.abs() 绝对值 Math.floor() 向下取整
	if(String(Math.floor(Math.abs(Number(i)))) !== i) continue; // 过滤不是非负整数的属性
    console.log(arr[index]);
}
如果算法依赖遍历的顺序,就不要使用for/in,为什么?

ECMAScript规范允许for/in以不同的顺序遍历对象的属性,第六章对象中提到过,一般情况下是按照创建的先后顺序来返回的,而数组循环返回的顺序是按照数组索引的升序。当数组中同时拥有对象属性和数组元素时,循环返回的顺序可能不是按照索引的大小顺序,而是按照创建的先后顺序,即按照对象循环返回的规则来。

ES5中ForEach()

ForEach()会按照索引的顺序传递给定义的一个函数。

// 使用forEach
const arr = [1,2,3,4,5];
let sum = 0;
arr.forEach((i) => {
    sum += i;
});
console.log(sum); // 15

7.6 数组方法

ES3中,定义了join()、reverse()、sort()、concat()、slice()、splice()、toString()、toLocaleString(),还有之前提到过的push()、pop()、unshift()、shift()。

ES5中,定义了9个新的数组方法来遍历、映射、过滤、检测、简化和搜索数组,分别是map()、filter()、every()、some()、reduce()、reduceRight()、indexOf()、lastIndexOf()和前面提到的forEach()。

ES5中大多数的方法第一个参数接收一个函数,并对数组的每个元素调用一次该函数。(会忽略稀疏数组中不存在的元素),并且大多数情况下,这个函数提供三个参数,分别为数组元素、数组索引和数组本身。

大多数ES5数组方法中的第二个参数是可选的,如果有第二个参数,则第一个参数--函数被看作是第二个参数的方法,也就是说第一个参数--函数中的this指向数组方法的第二个参数。

// Array.join()将数组中的所有元素都转换为字符串并连接在一起,可以指定一个可选的字符串来分隔数组的各个元素。如果不指定,默认为逗号。String.split()方法和Array.join()是逆向操作。
const arr1 = [1, 2, 3];
arr1.join(); // "1,2,3"
arr1.join('-'); // "1-2-3"
const arr2 = new Array(10); // 长度为10的空数组
arr2.join('-'); // '---------' 9个-连成的字符串
const str = "1-2-3";
str.split('-'); // ["1", "2", "3"]

// Array.reverse()方法将数组中的元素颠倒顺序,返回逆序的数组。只是在原先的数组中重现排列它们,并没有新建一个数组。
arr1.reverse(); // [3, 2, 1]

// Array.sort()方法将数组中的元素排序并返回排序后的数组。当不传参数时,数组元素以字母表顺序排序
const arr3 = new Array('banana', 'cherry', 'apple');
arr3.sort(); // ["apple", "banana", "cherry"]
// 如果数组中包含undefined元素,它们将会被排列到尾部
const arr4 = [undefined, 2, 1, 3, 'banana', 'cherry', 'apple'];
arr4.sort(); // [1, 2, 3, "apple", "banana", "cherry", undefined]
// 可以在sort()方法中传递一个比较函数,来按照函数所设定的比较方式来排序
const arr5 = [33, 4, 1111, 2222];
arr5.sort(); // [1111, 2222, 33, 4] 
arr5.sort(function (a, b) {
    // a表示的是前一个数,b表示的是后一个数,如果返回true,a和b保持原位置,如果返回false,则交换位置
    return a - b; 
}); // [4, 33, 1111, 2222]

// Array.concat()方法创建并返回一个新数组,它的元素包括原始数组的元素和concat()的每个参数。
const arr6 = [1, 2, 3];
arr6.concat(4, 5); // [1, 2, 3, 4, 5]
arr6.concat([4, 5]); // [1, 2, 3, 4, 5]
arr6.concat([4, 5], [6, 7]); // [1, 2, 3, 4, 5, 6, 7]
arr6.concat(4, [5, [6, 7]]); // [1, 2, 3, 4, 5, [6, 7]]

/**Array.slice()方法返回指定数组的一个片段或者子数组,不会修改原数组。它的两个参数分别指定了片段的开始和结	束位置。
*/
const arr7 = [1, 2, 3, 4, 5];
arr7.slice(0, 3);// [1, 2, 3]
// 如果只指定了一个参数,那么表示从这个参数开始的位置到数组结束的所有元素
arr7.slice(3); // [4, 5]
// 如果参数为负数,则表示从数组的最后一个元素的位置开始算起,比如-2,表示倒数第二个元素。
arr7.slice(1, -1); // [2, 3, 4]
arr7.slice(-3, -1); // [3, 4]

/** Array.splice()方法是在数组中插入或者删除元素的通用方法。
	第一个参数指定了插入或者删除的起始位置
	第二个参数指定了删除元素的个数,如果省略了该参数,那么表明从起点开始到数组结束的所有元素都将被删除。	
	Array.splice()方法返回由删除元素组成的数组,如果没有删除元素就返回一个空数组。
	紧随前2个参数值得任意个数的参数指定了需要插入到数组中的元素,从第一个参数指定的位置开始插入。
**/
const arr8 = [1, 2, 3, 4, 5, 6, 7, 8];
arr8.splice(4, 2); // [5, 6] 从下标4开始删除2个元素,并将这个2个元素返回
// 此时arr8数组变成了[1, 2, 3, 4, 7, 8]
arr8.splice(4); // [7, 8] 从下标4开始到数组结尾的所有元素
// 此时arr8数组变成了[1, 2, 3, 4]
arr8.splice(); // []
// 此时arr8数组仍然是[1, 2, 3, 4]
arr8.splice(2, 0, 9 ,10, 11); // []
// 此时数组[1, 2, 9, 10, 11, 3, 4]

// toString()方法会将每个元素转化为字符串,如有必要会调用元素的toString()方法,并输出用逗号间隔的字符串。
[1, 2, 3].toString(); // "1,2,3"
[1, [2, 'c']].toString(); // "1,2,c"
[new Date()].toString(); // "Sun Feb 23 2020 17:39:37 GMT+0800 (中国标准时间)"
// toLocaleString()方法是toString()方法的本地化版本
[new Date()].toLocaleString(); // "2020/2/23 下午5:39:31"

// forEach()无法像for循环那样使用相应的break语句,提前终于循环。如果要提前终于循环,只能在try块中抛出异常
function foreach(a, f, t) {
    try {
        a.forEach(f, t);
    } catch(e) {
        if (e === foreach.break) return;
        else throw e;
    }
}
foreach.break = new Error("StopIteration");

// map()方法将调用的数组的每个元素传递给指定的函数,并返回一个数组,它包含该函数的返回值。
const arr9 = [1, 2, 3];
const arr10 = arr9.map(function(item) {
    return item * item;
});
console.log(arr9); // [1, 2, 3]
console.log(arr10); // [1, 4, 9] map()返回的是新数组,它不修改原数组

// filter()方法返回的数组元素是调用的数组的一个子集
const arr11 = [1, 2, 3, 4, 5, 6];
const arr12 = arr11.filter(item => {
    return item > 3;
}); // [4, 5, 6]
// filter()会跳过稀疏数组种缺少的元素,返回的数组总是稠密的。
const arr13 = [,,,,1,,,,2];
const arr14 = arr13.filter(() => {
   return true; 
}); // [1, 2]

/** every()和some()方法是数组的逻辑判定
	它们对数组元素应用指定的函数进行判定,返回true或false
	every()方法当且仅当数组中所有元素调用判定函数都返回true时,它才返回true
	some()方法当数组中的元素调用判断函数只要有一个返回true,它就返回true
**/
const arr15 = [1, 2, 3, 4, 5];
arr15.every((item) => {
    return item > 0;
}); // true
arr15.every((item) => {
    console.log(item); // 打印了1,说明当有一个值返回false,整个every()方法返回了false就不再执行
    return item > 1;
}); // false
arr15.some((item) => {
    console.log(item); // 打印了1 2,说明当有一个值返回true时,整个some()方法返回true,就不再执行下去
    return item > 1;
}); // true

// reduce()和reduceRight()方法使用指定的函数将数组元素进行组合,生成单个值。
// reduce()方法有2个参数,第一个参数就是上述所说的函数,第二个参数是可选的,是一个传递给函数的初始值。
const arr16 = [1, 2, 3, 4, 5];
const sum1 = arr16.reduce((x, y) => {
    return x + y;
}, 0); // 15  0+1+2+3+4+5=15
const sum2 = arr16.reduce((x, y) => {
    return x + y;
}, 100); // 115  100+1+2+3+4+5=15
/**在空数组上,不带初始值参数调用reduce()将导致类型错误异常。
	如果调用它的时候只有一个值--数组只有一个元素并且没有指定初始值,或者有一个空数组并且指定了一个初始值,由于化简函数需要2个参数,这时只有一个,因此reduce()不会调用化简函数,只是简单的返回这个值。
**/
// redecue()方法从左至右执行化简函数,reduceRight()方法从右至少执行化简函数。

/** indexOf()和lastIndexOf()搜索整个数组中具有给定值的元素,返回找到的第一个元素的索引或者如果没有找到就返回-1。
	indexOf()从左往右开始搜索,lastIndexOf从右往左开始搜索。
	indexOf()和lastIndexOf()的第二个参数是可选的,指定数组从此索引开始搜索,此参数也可以是负数,相对于数组末尾的偏移量。
**/
const arr17 = [1, 2, 3, 1, 0, 5];
console.log(arr17.indexOf(1)); // 0
console.log(arr17.lastIndexOf(1)); // 3
console.log(arr17.indexOf(1, 1)); // 3 从索引为1的地方开始从左往右搜索1

7.7 数组类型

在ES5中,可以使用Array.isArray()来判断是否是数组。在ES3中,使用typeof和instanceof都会有各自的问题,解决方案是检查对象的类属性。

// ES5中的方法
Array.isArray([]); // true
Array.isArray({}); // false

typeof []; // "object"
[] instanceof Array; // true
({}) instanceof Array; // false
/**
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
这里使用instance虽然检测对了,但是这是在一种简单的情形之下。instanceof的问题在于当有多个窗体时,每个窗体的全局对象是不同的。将窗体A中的数组传递到窗体B中使用instanceof去检测这个数组实例是否是Array时,由于这个数组实例的原型链和窗体B毫无关系,因此返回false。
因此instanceof并不是一个可靠的检测数组的方法。
**/

const isArray = Function.isArray || function(o) {
    return typeof o === 'object' &&
        Object.prototype.toString.call(o) === '[object Array]';
}
isArray([]); // true
isArray({}); // false

7.8 类数组对象

Arguments对象就是一个类数组对象,还有一些dom方法,比如document.getElementsByTagName()也返回类数组对象。

/** 判断o是否是一个类数组对象
	字符串和函数有length属性,可以使用typeof过滤
	dom文本节点也有length属性,可以额外使用o.nodeType != 3将其排除
**/
function isArrayLike(o) {
    if (o && // 只要是数组o肯定存在,排除了像null、undefined等值
        typeof o === 'object' && // 是个对象,数组是对象的一种
        isFinite(o.length) && // length属性是有限数值
        o.length >= 0 && //length数组大于等于0
        o.length === Math.floor(0.length) && // length属性为整数 
        o.length < Math.pow(2, 32) // length属性小于2^32(数组的最大长度为2^32-1)
        ) {
        return true;
    } else {
        return false;
    }
}

Javascript中的数组方法是特意定义为通用的,因此它们在真正的数组和类数组对象上都能正常工作。在ES5中的所有数组方法都是通用的。在ES3中,除了toString()和toLocaleString()以外的所有方法也是通用的。concat是一个特例,虽然可以在类数组对象上使用,但是它并没有将那个对象扩充进返回的数组中。既然类数组对象没有继承自Array.prototype,那就不能再它们上面直接调用数组方法,可以间接使用Function.call方法调用。

const obj = {'0': 'a', '1': 'b', '2': 'c', length: 3};
Array.prototype.join.call(obj, '+'); // "a+b+c"
Array.prototype.slice.call(obj, 0); // ["a", "b", "c"] 真正的数组的副本
Array.prototype.map.call(obj, function(x) {
    return x.toUpperCase();
}); // ["A", "B", "C"]

// ES5中的数组方法是在Firefox1.5中引入的,Firefox还将这些方法在Array的构造函数上直接定义为函数。
Array.join(obj, '+');
Array.slice(obj, 0);
Array.map(obj, function(x) {
    return x.toUpperCase();
});

// 由于数组的静态方法不是在所有浏览中都存在,因此需要兼容所有浏览器可以这么写
Array.join = Array.join || function (a, sep) {
    Array.prototype.join.call(a, sep);
};
Array.slice = Array.slice || function (a, from, to) {
    return Array.prototype.slice.call(a, from, to);
};
Array.map = Array.map || function (a, f, thisArg) {
    return Array.prototype.map.call(a, f, thisArg);
}

7.9 作为数组的字符串

字符串的行为类似于只读的数组,字符串是不可变值,故把它们作为数组看待时,它们是只读的,数组的push()、sort()、reverse()和splice()方法在字符串上是无效的,会导致错误。除了用charAt()方法来访问字符串的单个字符之外,还可以使用方括号

// charAt()和方括号访问字符串的单个字符
var str = 'abcde';
str.charAt(0); // "a"
str[1] // "b"

var s = 'Javascript';
Array.prototype.join.call(s, " "); // "J a v a s c r i p t"
Array.prototype.filter.call(s, function(x) {
    return x.match(/[^aeiou]/);
}).join(""); // "Jvscrpt"

/**导致错误Uncaught TypeError: Cannot assign to read only property 'length' of object '[object String]'
**/
Array.prototype.push.call(s, "添加元素");

7.10 ES6中的数组

7.10.1 扩展运算符

扩展运算符是3个点(...)。将一个数组转为用逗号分隔的参数序列。

替换函数的apply方法

由于扩展运算符可以展开数组,所以不再需要apply方法。

// 计算数组中的最大值
const arr = [1, 3, 4, 56, 13];
// ES5
Math.max.apply(null, arr); // 56
//ES6
Math.max(...arr); // 56

// 将数组a添加到另一个数组b的尾部,返回数组a
const a = [1, 2, 3];
const b = [4, 5, 6];
// ES5
Array.prototype.push.apply(a, b); // [1, 2, 3, 4, 5, 6]
// ES6
const c = [1, 2, 3];
const d = [4, 5, 6];
c.push(...d);// [1, 2, 3, 4, 5, 6]
扩展运算符的运用
  1. 复制数组
  2. 合并数组
  3. 与解构赋值结合
  4. 字符串
  5. 实现Iterator接口的函数
  6. Map和Set结构,Generator函数

7.10.2 Array.from()

Array.from()将两类对象转为真正的数组:类数组对象和可遍历对象(包括ES6中新增的Set和Map)。

let arrLike = {
	'0': 'a',
    '1': 'b',
	'2': 'c',
	'length': 3,
};
// ES5
Array.prototype.slice.call(arrLike); // ["a", "b", "c"]
// ES6
Array.from(arrLike); // ["a", "b", "c"]

Array.from()还接收第二个可选参数,作用类似于map()方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

7.10.3 Array.of()

Array.of()方法将一组值转化为数组。

这个方法主要是弥补数组构造函数Array()的不足,前面我们已经提到过,使用Array()的三种方式创建数组,这是有缺陷的。

Array.of(1, 2, 3); // [1,2,3]
Array.of(3); // [3]

new Array(3); // [,,,]

7.10.4 数组实例的copyWithin()

数组实例的copyWithin()方法,在当前数组内部,将指定位置的数组元素复制至其他位置(会覆盖原有数组元素),然后返回当前数组。此方法有三个参数:

1.target(必需):从该位置开始替换数据。

2.start(可选):从该位置开始读取数据。

3.end(可选):到该位置停止读取数据。

[1, 2, 3, 4, 5].copyWithin(0, 3); // [4, 5, 3, 4, 5]

7.10.5 find()和findIndex()

7.10.6 数组实例的fill()

使用给定值填充一个数组

[1, 2, 3].fill(100); // [100, 100, 100]

fill()还接收第二个和第三个可选参数,用来指定填充的起始位置和结束位置。

[1, 2, 3, 4, 5].fill(100, 1, 3); // [1, 100, 100, 4, 5]

7.10.7 数组实例的entries(),keys(),values()

entries()返回数组的键值对遍历器对象

keys()返回数组的键遍历器对象

values()返回数组的值遍历器对象


7.10.8 数组实例的includes()

表示某个数组是否包含给定的值,返回布尔值。

[1, NaN, 3].includes(NaN); // true
// 使用indexOf(),会导致NaN的误判
[1, NaN, 3].indexOf(NaN) !== -1; // false

第二个可选参数,表示从哪个位置开始搜索。

[1, NaN, 3, 4, 5].includes(NaN, 2); // false

7.10.9 flat()和flatMap()

flat()将嵌套的数组拉平,转化为一个一维数组,该方法返回一个新数组,对原数组不产生影响。

[1, 2, [3, 4]].flat(); // [1, 2, 3, 4]
// 只会拉平一层
[1, [2, [3, 4]]].flat(); // [1, 2, [3, 4]]

如果想要拉平指定层数,可以传入一个整数参数,如果不知道要拉平多少层,可以传入Infinity关键字作为参数。

[1, [2, [3, 4]]].flat(2); // [1, 2, 3, 4]
[1, [2, [3, 4]]].flat(Infinity); // [1, 2, 3, 4]

flatMap()方法对原数组先执行一个map()操作,再执行flat()方法拉平一层。

/**
	先执行了map()操作[[1, 10], [2, 20], [3, 30]]
	再执行flat()操作[1, 10, 2, 20, 3, 30]
**/
[1,2,3].flatMap(x => [x, x*10]); // [1, 10, 2, 20, 3, 30]

flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

7.10.10 空位

7.10.11 Array.prototype.sort()的排序稳定性