集合引用类型
Object
到目前为止,大多数引用值的示例使用的是 Object 类型。Object 是 ECMAScript 中最常用的类 型之一。虽然 Object 的实例没有多少功能,但很适合存储和在应用程序间交换数据。
显式地创建 Object 的实例有两种方式。第一种是使用 new 操作符和 Object 构造函数、另一种方式是使用对象字面量(object literal)表示法。
let obj = new Object();
obj.name = 'Joker';
obj.age = 18;
let obj = {
name: 'Joker',
age: 18,
};
let obj2 = {};
obj2.name = 'Joker';
obj2.age = 18;
存取属性有两种方式:第一种通过点语法、另一种使用中括号。
let obj = {
name: 'Joker',
age: 18,
};
console.log(obj.name); // Joker
console.log(obj['name']); // Joker
从功能上讲,这两种存取属性的方式没有区别。使用中括号的主要优势就是可以通过变量访问属性,
let propertyName = 'name';
console.log(obj[propertyName]); // "Joker"
Array
除了 Object,Array 应该就是 ECMAScript 中最常用的类型了。ECMAScript 数组跟其他编程语言的数组有很大区别。跟其他语言中的数组一样,ECMAScript 数组也是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。这意味着可以创建一个数组,它的第一个元素是字符串,第二个元素是数值,第三个是对象。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
创建数组
有几种基本的方式可以创建数组。一种是使用 Array 构造函数、另一种是数组字面量。
const arr = new Array('red', 'blue', 'green');
console.log(arr); // [ 'red', 'blue', 'green' ]
// 可以省略 new 操作符
const num = Array(1, 2, 3);
console.log(num); // [ 1, 2, 3 ]
let colors = ['red', 'blue', 'green']; // 创建一个包含 3 个元素的数组
let names = []; // 创建一个空数组
let values = [1, 2]; // 创建一个包含 2 个元素的数组
创建数组时可以给构造函数传值,若是 number 类型,则指定一个数组长度的空数组,若是其他类型的,则会创建一个只包含该元素的数组。
let colors = new Array(3); // (3) [empty × 3] 数组长度 3 的空数组
let names = new Array('Greg'); // 创建一个只包含一个元素,即字符串"Greg"的数组
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()
和 of()
。from()
用于将类数组结构转换为数组实例,而 of()
用于将一组参数转换为数组实例。
// 字符串会被拆分为单字符数组
console.log(Array.from('Matt')); // ["M", "a", "t", "t"]
// 可以使用 from()将集合和映射转换为一个新数组
const m = new Map().set(1, 2).set(3, 4);
const s = new Set().add(1).add(2).add(3).add(4);
console.log(Array.from(m)); // [[1, 2], [3, 4]]
console.log(Array.from(s)); // [1, 2, 3, 4]
// Array.from()对现有数组执行浅拷贝
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1);
console.log(a1 === a2); // false
// 类数组转换成数组
Array.from(arguments);
// from()也能转换带有必要属性的自定义对象(类数组模拟,键是数值,带有 length 属性)
let books = {
0: 'JavaScript',
1: 'Java',
2: 'Python',
length: 3,
};
console.log(Array.from(books)); // [ 'JavaScript', 'Java', 'Python' ]
// from()还接收第二个可选的映射函数参数。增强新数组的值。
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x ** 2);
console.log(a2); // [ 1, 4, 9, 16 ]
// from()还可以接收第三个可选参数,用于指定映射函数中 this 的值。但这个重写的 this 值在箭头函数中不适用。
const a3 = Array.from(
a1,
function (x) {
return x ** this.num;
},
{ num: 2 },
);
console.log(a3); // [1, 4, 9, 16]
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
数组空位
第一种使用 Array 构造函数创建,传入数值,变可以创建多少长度的空位数组、另一种使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6 规范重新定义了该如何处理这些空位。
const arr1 = new Array(5);
console.log(arr1); // [ <5 empty items> ]
const arr2 = [, , , , ,]; // 创建包含 5 个元素的数组
console.log(arr2); // [ <5 empty items> ]
ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这 些空位当成存在的元素,只不过值为 undefined:
const arr = new Array(5);
for (const item of arr) {
console.log(arr); // undefined
}
ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异:
const arr = [1, , , , 5];
// map()、forEach()会跳过空位置
console.log(arr.map(() => 6)); // [ 6, <3 empty items>, 6 ]
arr.forEach(item => {
console.log(item); // 1 5
});
// join()视空位置为空字符串
console.log(arr.join('-')); // 1----5
由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用 undefined
值代替。
数组 索引
要取得或设置数组的值,需要使用中括号并提供相应值的数字索引,如下所示:
let colors = ['red', 'blue', 'green']; // 定义一个字符串数组
console.log(colors[0]); // 显示第一项
colors[2] = 'black'; // 修改第三项
colors[3] = 'brown'; // 添加第四项
数组 length 属性的独特之处在于,它不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。
let colors = ['red', 'blue', 'green'];
colors.length = colors.length - 1; // 删除末尾元素
console.log(colors); // [ 'red', 'blue' ]
colors[colors.length] = 'yellow'; // 向末尾添加元素
console.log(colors); // [ 'red', 'blue', 'yellow' ]
数组最多可以包含 4 294 967 295 个元素,这对于大多数编程任务应该足够了。如果尝试添加更多项,则会导致抛出错误。以这个最大值作为初始值创建数组,可能导致脚本运行时间过长的错误。
检查数组
一个经典的 ECMAScript 问题是判断一个对象是不是数组。在只有一个网页(因而只有一个全局作 用域)的情况下,使用 instanceof 操作符就足矣:
if (value instanceof Array) {
// 操作数组
}
但是,在涉及多个框架或多个全局执行上下文的环境中,可能会存在不同版本的构造函数,导致 instanceof 的问题。
// 在框架 A 中创建数组
const arrInFrameA = [1, 2, 3];
// 在框架 B 中使用 instanceof 检查数组
console.log(arrInFrameA instanceof Array); // true
// 在框架 B 中创建数组
const arrInFrameB = [4, 5, 6];
// 在框架 A 中使用 instanceof 检查数组
console.log(arrInFrameB instanceof Array); // false
这是因为在 JavaScript 中,instanceof 会检查对象的原型链,而在不同的框架中,可能存在不同的全局对象,它们有不同的原型链。因此,两个数组虽然内容相同,但由于它们是在不同的框架中创建的,其原型链可能不一样,导致 instanceof 的结果不同。
为解决这个问题,ECMAScript 提供了 Array.isArray()
方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
if (Array.isArray(value)) {
// 操作数组
}
迭代器方法
在 ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys()
、values()
和entries()
。keys()
返回数组索引的迭代器,values()
返回数组元素的迭代器,而 entries()
返回索引/值对的迭代器:
let colors = ['red', 'blue', 'green'];
const keys = Array.from(colors.keys());
const values = Array.from(colors.values());
const entries = Array.from(colors.entries());
console.log(keys); // [ 0, 1, 2 ]
console.log(values); // [ 'red', 'blue', 'green' ]
console.log(entries); // [ [ 0, 'red' ], [ 1, 'blue' ], [ 2, 'green' ] ]
复制和填充方法
ES6 新增了两个方法:copyWithin()
和 fill()
。这两个方法的
函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。这两个方法修改了原数组,而不是创建一个新的数组,且不会改变数组的大小。
copyWithin
是数组对象的一个方法,用于在数组内部复制一部分元素,并将其粘贴到另一个位置,覆盖原有的元素。
/**
* @param target 复制到目标位置,表示从该位置开始粘贴复制的元素。
* @param start 复制的起始位置,表示从该位置开始复制元素。
* @param end 复制的结束位置(可选),表示在该位置之前停止复制元素。如果省略,则默认为数组的末尾。
*/
array.copyWithin(target, start, end);
const numbers = [1, 2, 3, 4, 5];
// 将索引为 0 到 2(不包括2)的元素复制到索引为 2 的位置
numbers.copyWithin(2, 0, 2);
// 原来的元素被覆盖
console.log(numbers); // 输出: [1, 2, 1, 2, 5]
fill
是数组对象的一个方法,用于将数组的所有元素替换为指定的静态值。
/**
* @param value 要用于填充数组的值。
* @param start 开始填充的起始位置(可选,默认为 0)
* @param end 填充的结束位置(可选,默认为数组的末尾)。
*/
array.fill(value, start, end);
const numbers = [1, 2, 3, 4, 5];
// 将数组的所有元素替换为 0
numbers.fill(0);
console.log(numbers); // 输出: [0, 0, 0, 0, 0]
// 将索引为 1 到 3(不包括3)的元素替换为 0
numbers.fill(2, 1, 3);
console.log(numbers); // 输出: [0, 2, 2, 0, 0]
转换方法
前面提到过,所有对象都有 toLocaleString()
、toString()
和 valueOf()
方法。它们是 JavaScript 中数组对象的三个方法,用于获取数组的字符串表示或基本值。
toLocaleString()
方法返回一个表示数组 元素的本地化字符串。数组中的每个元素都会被转换为字符串,并且会使用本地化规则,例如逗号分隔的数字和日期格式。
const numbers = [12345.6789, 98765.4321];
console.log(numbers.toLocaleString());
// 根据浏览器或系统的本地化规则输出字符串,例如 "12,345.679,8765.432"
toString()
方法返回一个表示数组元素的字符串。数组中的每个元素都会被转换为字符串,并用逗号分隔。
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.toString()); // 输出: "1,2,3,4,5"
valueOf()
方法返回数组对象的原始值。在数组对象上调用 valueOf()
时,返回的是数组对象本身。
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.valueOf()); // 输出: [1, 2, 3, 4, 5]
如果数组中某一项是 null
或 undefined
,则在 join()
、toLocaleString()
、toString()
和 valueOf()
返回的结果中会以空字符串表示。
栈方法
ECMAScript 给数组提供几个方法,让它看起来像是另外一种数据结构。数组对象可以像栈一样,也就是一种限制插入和删除项的数据结构。栈是一种后进先出(LIFO,Last-In-First-Out)的结构,也就是最近添加的项先被删除。数据项的插入(称为推入,push)和删除(称为弹出,pop)只在栈的一个地方发生,即栈顶。ECMAScript 数组提供了 push()
和 pop()
方法,以实现类似栈的行为。
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop()
方法则用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项。
let color = ['red', 'blue', 'green'];
// 数组末尾添加
color.push('pink');
console.log(color); // ['red', 'blue', 'green', 'pink']
// 删除数组末尾元素
color.pop();
console.log(color); // ['red', 'blue', 'green'];
队列方法
就像栈是以 LIFO 形式限制访问的数据结构一样,队列以先进先出(FIFO,First-In-First-Out)形式限制访问。队列在列表末尾添加数据,但从列表开头获取数据。因为有了在数据末尾添加数据的 push()
方法,所以要模拟队列就差一个从数组开头取得数据的方法了。这个数组方法叫 shift()
,它会删除数组的第一项并返回它,然后数组长度减 1。
let color = ['red', 'blue', 'green'];
// 删除数组第一项
let item = color.shift();
console.log(item); // 'red'
console.log(color); // [ 'blue', 'green' ]
ECMAScript 也为数组提供了 unshift()
方法。顾名思义,unshift()
就是执行跟 shift()
相反的操作:在数组开头添加任意多个值,然后返回新的数组长度。通过使用 unshift()
和 pop()
,可以在相反方向上模拟队列。
let color = ['red', 'blue', 'green'];
color.unshift('white', 'pink');
console.log(color); // [ 'white', 'pink', 'red', 'blue', 'green' ]
// 删除最后一项
color.pop();
console.log(color); // [ 'white', 'pink', 'red', 'blue' ]