第六章—集合引用类型
本章内容:
- 对象
- 数组与定型数组
- Map、WeakMap、Set 以及WeakSet 类型
6.1 Object
显式地创建Object 的实例有两种方式:
- 使用new 操作符和Object 构造函数:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
- 使用对象字面量(object literal)表示法,对象字面量是对象定义的简写形式,目的是为了简化包含大量属性的对象的创建:
let person = {
name: "Nicholas",
age: 29
};
逗号用于在对象字面量中分隔属性,因此字符串"Nicholas"后面有一个逗号,而29 后面没有,因为age 是这个对象的最后一个属性。
数值类型的属性名会被自动转换为字符串。
另外一种利用对象字面量表示法定义对象的方法:
let person = {}; // 与new Object()相同
person.name = "Nicholas";
person.age = 29;
使用对象字面量来封装给函数传递的参数:
function displayInfo(args) {
let output = "";
if (typeof args.name == "string"){
output += "Name: " + args.name + "\n";
}
if (typeof args.age == "number") {
output += "Age: " + args.age + "\n";
}
alert(output);
}
displayInfo({
name: "Nicholas",
age: 29
});
displayInfo({
name: "Greg"
});
如上所示,将函数所需的name和age参数封装在一起传递给函数。
注意 这种模式非常适合函数有大量可选参数的情况。一般来说,命名参数更直观,但在
可选参数过多的时候就显得笨拙了。最好的方式是对必选参数使用命名参数(直接在函数接收的形参那里写出来),再通过一个 对象字面量来封装多个可选参数。
对象的属性可以通过点号和中括号修改访问,中括号时要用字符串形式:
console.log(person["name"]); // "Nicholas"
console.log(person.name); // "Nicholas"
中括号形式可以借助变量来访问属性:
let propertyName = "name";
console.log(person[propertyName]); // "Nicholas"
如上例,通过变量值访问了name属性。
如果属性名中包含可能会导致语法错误的字符,或者包含关键字/保留字时,也可以使用中括号语法。比如:
person["first name"] = "Nicholas";
因为"first name"中包含一个空格,所以不能使用点语法来访问。
注意 第8 章将更全面、深入地介绍Object 类型。
6.2 Array
ECMAScript 数组也是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。这意味着可以创建一个数组,它的第一个元素是字符串,第二个元素是数值,第三个是对象。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
6.2.1 创建数组
方法1,使用Array构造函数:
let colors = new Array();
为构造函数传入一个数值,可以创建指定长度的数组:
let colors = new Array(20);
也可以传递给构造函数要保存的元素,比如,下面的代码会创建一个包含3 个字符串值的数组:
let colors = new Array("red", "blue", "green");
在使用Array 构造函数时,可以省略new 操作符,结果是一致的。
方法2,创建数组的方式是使用数组字面量(array literal)表示法。数组字面量是在中括号中包含以逗号分隔的元素列表:
let colors = ["red", "blue", "green"]; // 创建一个包含3 个元素的数组
let names = []; // 创建一个空数组
let values = [1,2,]; // 创建一个包含2 个元素的数组
在数组最后一个值后面加逗号不影响,即第三行生成数组 [1,2]。
Array 构造函数还有两个ES6 新增的用于创建数组的静态方法:from()
和of()
。from()
用于将类数组结构转换为数组实例,而of()
用于将一组参数转换为数组实例。
Array.from()
的第一个参数是一个类数组对象,即任何可迭代的结构,或者有一个length 属性和可索引元素的结构。这种方式可用于很多场合:
// 字符串会被拆分为单字符数组
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.of()
可以把一组参数转换为数组。
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined)); // [undefined]
6.2.2 数组空位
使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6 规范重新定义了该如何处理这些空位。
可以像下面这样创建一个空位数组:
const options = [,,,,,]; // 创建包含5 个元素的数组
console.log(options.length); // 5
console.log(options); // [,,,,,]
ES6 新增方法普遍将这些空位当成存在的元素,只不过值为undefined。
const options = [1,,,,5];
for (const option of options) {
console.log(option === undefined);
}
// false
// true
// true
// true
// false
注意 由于不同ES版本行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用undefined 值代替。
6.2.3 数组索引
索引从0开始,
在中括号中提供的索引表示要访问的值。如果索引小于数组包含的元素数,则返回存储在相应位置的元素,如果把一个值设置给超过数组最大索引的索引,就像示例中的colors[3],则数组长度会自动扩展到该索引值加1(示例中设置的索引3,所以数组长度变成了4)。
let colors = ["red", "blue", "green"]; // 定义一个字符串数组
alert(colors[0]); // 显示第一项
colors[2] = "black"; // 修改第三项
colors[3] = "brown"; // 添加第四项
通过修改length 属性,可以从数组末尾删除或添加元素。
let colors = ["red", "blue", "green"]; // 创建一个包含3 个字符串的数组
colors.length = 2;
alert(colors[2]); // undefined
使用length 属性可以方便地向数组末尾添加元素,如下例所示:
let colors = ["red", "blue", "green"]; // 创建一个包含3 个字符串的数组
colors[colors.length] = "black"; // 添加一种颜色(位置3)
colors[colors.length] = "brown"; // 再添加一种颜色(位置4)
超出索引部分会直接填充undefined:
let colors = ["red", "blue", "green"]; // 创建一个包含3 个字符串的数组
colors[99] = "black"; // 添加一种颜色(位置99)
alert(colors.length); // 100
这里,colors 数组有一个值被插入到位置99,结果新length 就变成了100(99 + 1)。这中间的所有元素,即位置3~98,实际上并不存在,因此在访问时会返回undefined。
6.2.4 检测数组
ECMAScript 提供了Array.isArray()方法,这个方法的目的就是确定一个值是
否为数组,而不用管它是在哪个全局执行上下文中创建的。
if (Array.isArray(value)){
// 操作数组
}
6.2.5 迭代器方法
在ES6 中,Array 的原型上暴露了3 个用于检索数组内容的方法:keys()
、values()
和entries()
。keys()返回数组索引的迭代器,values()返回数组元素的迭代器,而entries()返回索引/值对的迭代器。
示例:
const a = ["foo", "bar", "baz", "qux"];
// 因为这些方法都返回迭代器,所以可以将它们的内容
// 通过Array.from()直接转换为数组实例
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());
console.log(aKeys); // [0, 1, 2, 3]
console.log(aValues); // ["foo", "bar", "baz", "qux"]
console.log(aEntries); // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]
使用ES6 的解构可以非常容易地在循环中拆分键/值对:
const a = ["foo", "bar", "baz", "qux"];
for (const [idx, element] of a.entries()) {
alert(idx);
alert(element);
}
// 0
// foo
// 1
// bar
// 2
// baz
// 3
// qux
6.2.6 复制和填充方法
ES6 新增了两个方法:批量复制方法copyWithin()
,以及填充数组方法fill()
。这两个方法的函数签名类似,都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小。
使用fill()方法可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充的位置,它是可选的。如果不提供结束索引,则一直填充到数组末尾。负值索引从数组末尾开始计算。也可以将负索引想象成数组长度加上它得到的一个正索引:
const zeroes = [0, 0, 0, 0, 0];
// 用5 填充整个数组
zeroes.fill(5);
console.log(zeroes); // [5, 5, 5, 5, 5]
zeroes.fill(0); // 重置
// 用6 填充索引大于等于3 的元素
zeroes.fill(6, 3);
console.log(zeroes); // [0, 0, 0, 6, 6]
zeroes.fill(0); // 重置
// 用7 填充索引大于等于1 且小于3 的元素
zeroes.fill(7, 1, 3);
console.log(zeroes); // [0, 7, 7, 0, 0];
zeroes.fill(0); // 重置
fill()静默忽略超出数组边界、零长度及方向相反的索引范围(超出的范围内不会执行):
const zeroes = [0, 0, 0, 0, 0];
// 索引过低,忽略
zeroes.fill(1, -10, -6);
console.log(zeroes); // [0, 0, 0, 0, 0]
// 索引过高,忽略
zeroes.fill(1, 10, 15);
console.log(zeroes); // [0, 0, 0, 0, 0]
// 索引反向,忽略
zeroes.fill(2, 4, 2);
console.log(zeroes); // [0, 0, 0, 0, 0]
// 索引部分可用,填充可用部分
zeroes.fill(4, 3, 10)
console.log(zeroes); // [0, 0, 0, 4, 4]
copyWithin()
会按照指定范围浅复制数组中的部分内容,然后将它们插入到指
定索引开始的位置。开始索引和结束索引则与fill()使用同样的计算方法
第一个参数表示要插入的位置,必须有。起始位置和终止位置可以省略,省略时默认从索引0开始:
let ints,
reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
// 从ints 中复制索引0 开始的内容,插入到索引5 开始的位置
// 在源索引或目标索引到达数组边界时停止
ints.copyWithin(5);
console.log(ints); // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
reset();
// 从ints 中复制索引5 开始的内容,插入到索引0 开始的位置
ints.copyWithin(0, 5);
console.log(ints); // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
值得注意,不会出现复制修改后的值的问题
// JavaScript 引擎在插值前会完整复制范围内的值
// 因此复制期间不存在重写的风险
ints.copyWithin(2, 0, 6);
alert(ints); // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9]
reset();
如上例,复制的值都是原始的值,而不会复制在这个过程中的修改后的值。
6.2.7 转换方法
toLocaleString()
、toString()
和valueOf()
方法
valueOf()
返回的还是数组本身。而toString()
返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串,就是对数组的每个值都会调用其toString()方法,并使用逗号分隔,以得到最终的字符串。
let array2 = ['Hello', 'World'];
console.log(array2.valueOf()); // ["Hello", "World"]
console.log(array2.toString()); // Hello,World
alert(array2) // Hello,World
用alert()显示数组,因为alert()期待字符串,所以会在后台调用数组的toString()方法,从而得到跟toString()一样的结果。
toLocaleString()方法也可能返回跟toString()和valueOf()相同的结果,但也不一定。在调用数组的toLocaleString()方法时,会调用数组每个值的toLocaleString()方法,而不是toString()方法。如果这两个方法结果不一致时,最终结果也就不一致。
除了默认的逗号分隔外,可以通过join()
方法,使用不同的分隔符连接数组值。
join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
注意 如果数组中某一项是null 或undefined,则在join()、toLocaleString()、
toString()和valueOf()返回的结果中会以空字符串表示。
6.2.8 栈方法
ECMAScript 数组提供了push()
和pop()
方法,以实现类似栈的行为。
push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop()方法则用于删除数组的最后一项,同时减少数组的length 值,返回被删除的项。
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2
count = colors.push("black"); // 再推入一项
alert(count); // 3
let item = colors.pop(); // 取得最后一项
alert(item); // black
alert(colors.length); // 2
6.2.9 队列方法
先进的方法有push(),先出的方法则是shift()
,它会删除数组的第一项并返回它,然后数组长度减1。使用shift()和push(),可以把数组当成队列来使用,实现先进先出的效果。
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2
count = colors.push("black"); // 再推入一项
alert(count); // 3
let item = colors.shift(); // 取得第一项
alert(item); // red
alert(colors.length); // 2
ECMAScript 也为数组提供了unshift()
方法。顾名思义,unshift()就是执行跟shift()相反的操作:在数组开头添加任意多个值,然后返回新的数组长度。
通过使用unshift()和pop(),可以在相反方向上模拟队列,即在数组开头添加新数据,在数组末尾取得数据。
6.2.10 排序方法
数组有两个方法可以用来对元素重新排序:reverse()
和sort()
。
reverse()
方法就是将数组元素反向排列
let array3 = [1,3,2,0];
console.log(array3.reverse()) // [0, 2, 3, 1]
默认情况下,sort()
会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。为此,sort()会在每一项上调用String()转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序。
因此一些情况会出现问题:
let values = [0, 1, 5, 10, 15];
values.sort();
alert(values); // 0,1,10,15,5
没有按照大小顺序正常排序,所以需要为sort()方法指定一个比较函数来进行判断,例如:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
此外,这个比较函数还可简写为一个箭头函数:
let values = [0, 1, 5, 10, 15];
values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
alert(values); // 15,10,5,1,0
return的值可以这样理解:
- return 0:不交换位置,不排序
- return 1:交换位置
- return -1:不交换位置
6.2.11 操作方法
concat()
方法可以在现有数组全部元素基础上创建一个新数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果传入一个或多个数组,则concat()会把这些数组的每一项都添加到结果数组。如果参数不是数组,则直接把它们添加到结果数组末尾。
在参数数组上指定一个特殊的符号:Symbol.isConcat-Spreadable
。这个符号能够阻止concat()打平参数数组,
- 设为false时,会加参数数组整体作为一个元素与原数组的副本连接。
例 :
let colors = ["red", "green", "blue"];
let newColors = ["black", "brown"];
newColors[Symbol.isConcatSpreadable] = false;
// 强制不打平数组
let colors2 = colors.concat("yellow", newColors);
console.log(colors2); // ["red", "green", "blue", "yellow", ["black", "brown"]]
- 设为true时,会将参数数组的内部元素与原数组副本连接(打平参数数组)。
slice()方法
方法slice()
用于创建一个包含原有数组中一个或多个元素的新数组。slice()方法可以接收一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则slice()返回从开始索引到结束索引对应的所有元素,其中不包含结束索引对应的元素。这个操作不影响原始数组。
注意
如果slice()的参数有负值,那么就以数值长度加上这个负值的结果确定位置。比如,在包含5个元素的数组上调用slice(-2,-1),就相当于调用slice(3,4)。如果结 束位置小于开始位置,则返回空数组。
splice()方法
splice()的主要目的是在数组中间插入元素,但有3 种不同的方式使用这个方法:
- 删除。需要给splice()传2 个参数:要删除的第一个元素的位置和要删除的元素数量。可以从数组中删除任意多个元素,比如splice(0, 2)会删除前两个元素。
- 插入。需要给splice()传3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。第三个参数之后还可以传第四个、第五个参数,乃至任意多 个要插入的元素。比如,splice(2, 0, “red”, “green”)会从数组位置2 开始插入字符串 “red"和"green”。
- 替换。splice()在删除元素的同时可以在指定位置插入新元素,同样要传入3 个参数:开始位 置、要删除元素的数量和要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量 一致。比如,splice(2, 1, “red”, “green”)会在位置2 删除一个元素,然后从该位置开始 向数组中插入"red"和"green"。
6.2.12 搜索和位置方法
断言函数
断言函数接收3 个参数:元素、索引和数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find()
和findIndex()
方法使用了断言函数。这两个方法都从数组的最小索引开始。find()返回第一个匹配的元素,findIndex()返回第一个匹配元素的索引。
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
alert(people.find((element, index, array) => element.age < 28));
// {name: "Matt", age: 27}
找到匹配项后,这两个方法都不再继续搜索。
6.2.13 迭代方法
数组的5 个迭代方法如下。
every()
:对数组每一项都运行传入的函数,如果对每一项函数都返回true,则这个方法返回true。filter()
:对数组每一项都运行传入的函数,函数返回true 的项会组成数组之后返回。forEach()
:对数组每一项都运行传入的函数,没有返回值。map()
:对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。some()
:对数组每一项都运行传入的函数,如果有一项函数返回true,则这个方法返回true。
这些方法都不改变调用它们的数组。
map()
方法会返回一个数组。这个数组的每一项都是对原始数组中同样位置的元素运行传入函数而返回的结果。例如,可以将一个数组中的每一项都乘以2,并返回包含所有结果的数组,如下所示:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
alert(mapResult); // 2,4,6,8,10,8,6,4,2
6.2.14 归并方法
ECMAScript 为数组提供了两个归并方法:reduce()
和reduceRight()
。这两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。reduce()方法从数组第一项开始遍历到最后一项。而reduceRight()从最后一项开始遍历至第一项。
可以使用reduce()函数执行累加数组中所有数值的操作,比如:
let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => prev + cur);
alert(sum); // 15
第一次执行归并函数时,prev 是1,cur 是2。第二次执行时,prev 是3(1 + 2),cur 是3(数组第三项)。如此递进,直到把所有项都遍历一次,最后返回归并结果。
6.4 Map
用于储存键值对。
6.4.1 基本API
使用new 关键字和Map 构造函数可以创建一个空映射:
const m = new Map();
在创建的同时初始化实例的方法为,给Map 构造函数传入一个可迭代对象,需要包含键/值对数组。
// 使用嵌套数组初始化映射
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
alert(m1.size); // 3
初始化时的数组对象包含一个空数组时:
// 映射期待的键/值对,无论是否提供
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined
初始化之后,可以使用set()
方法再添加键/值对。另外,可以使用get()
和has()
进行查询,可以通过size
属性获取映射中的键/值对的数量,还可以使用delete()
和clear()
删除值。
m.delete("firstName"); // 只删除这一个键/值对
m.clear(); // 清除这个映射实例中的所有键/值对
set()
方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明:
const m = new Map().set("key1", "val1");
m.set("key2", "val2")
.set("key3", "val3");
alert(m.size); // 3
与Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何JavaScript 数据类型作为键。
6.4.2 顺序与迭代
与Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
for (let pair of m.entries()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
entries()
是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
console.log([...m]); // [[key1,val1],[key2,val2],[key3,val3]]
keys()
和values()
分别返回以插入顺序生成键和值的迭代器:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
for (let key of m.keys()) {
alert(key);
}
// key1
// key2
// key3
for (let key of m.values()) {
alert(key);
}
// value1
// value2
// value3
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。
即可以修改键,但还是需要原始值来进行调用:
for (let key of m1.keys()) {
key = "newKey";
alert(key); // newKey
alert(m1.get("key1")); // val1
alert(m1.get('newKey')); //undefined
}
如上所示,调用原始的键名可以获得对应的值,但调用新的键名就无法获得对应的值。
结合哈希表想一下,应该是数据存放在根据哈希函数和初始的键算出的地址上,即使修改也并不会将数据移动到新的键对应的地址,所以使用新的键时,算出的地址上没有数据就返回undefined。
6.4.3 选择Object 还是Map
map是键值的映射,对象是作为属性进行保存。
- 内存占用: 给定固定大小的内存,Map 大约可以比Object 多存储50%的键/值对。
- 插入性能:向Object 和Map 中插入新键/值对的消耗大致相当,Map稍快一点。
- 查找速度:如果只包含少量键/值对,则Object 有时候速度更快。在把Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择Object 更好一些。
- 删除性能:使用delete 删除Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为undefined 或null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择Map。
对象删除属性不方便。
6.5 WeakMap
WeakMap 是Map 的“兄弟”类型,其API 也是Map 的子集。WeakMap 中的“weak”(弱),描述的是JavaScript 垃圾回收程序对待“弱映射”中键的方式。
6.5.1 基本API
可以使用new 关键字实例化一个空的WeakMap:
const wm = new WeakMap();
弱映射中的键只能是Object 或者继承自Object 的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。
即如下所示,只能使用对象来作为键:
const key1 = {id: 1},
key2 = {id: 2},
key3 = {id: 3};
// 使用嵌套数组初始化弱映射
const wm1 = new WeakMap([
[key1, "val1"],
[key2, "val2"],
[key3, "val3"]
]);
alert(wm1.get(key1)); // val1
alert(wm1.get(key2)); // val2
alert(wm1.get(key3)); // val3
键不是对象时,报错:
// 初始化是全有或全无的操作
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
[key1, "val1"],
["BADKEY", "val2"],
[key3, "val3"]
]);
// TypeError: Invalid value used as WeakMap key
typeof wm2;
// ReferenceError: wm2 is not defined
初始化之后可以使用set()
再添加键/值对,可以使用get()
和has()
查询,还可以使用delete()
删除。
6.5.2 弱键
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
const wm = new WeakMap();
wm.set({}, "val");
set()方法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。
即当这个对象键没有引用时,会被回收,导致这个键值对也会被回收。
6.5.3 不可迭代
因为WeakMap 中的键/值对任何时候都可能被销毁,所以并没有给WeakMap提供迭代键值对的能力。
所以无法迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。
即便代码可以访问WeakMap 实例,也没办法看到其中的内容。
WeakMap实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
还有一些关于私有变量闭包和DOM 节点元数据知识,见P171
6.6 Set
6.6.1 基本API
使用new 关键字和Set 构造函数可以创建一个空集合:
const m = new Set();
为Set()构造函数传入一个包含要添加到集合实例的元素构成的可迭代对象来初始化实例:
// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
alert(s1.size); // 3
初始化之后,可以使用add()
增加值,使用has()
查询,通过size
取得元素数量,以及使用delete()
和clear()
删除元素。
delete()返回一个布尔值,表示集合中是否存在要删除的值
add()返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化:
const s = new Set().add("val1");
s.add("val2")
.add("val3");
alert(s.size); // 3
与Map 类似,Set 可以包含任何JavaScript 数据类型作为值。
作为值加入集合的对象后进行的修改也会保留:
const s = new Set();
const objVal = {},
arrVal = [];
s.add(objVal);
s.add(arrVal);
objVal.bar = "bar";
arrVal.push("bar");
alert(s.has(objVal)); // true
alert(s.has(arrVal)); // true
for (const i of s) console.log(i);
// {bar: "bar"}
// ["bar"]
6.6.2 顺序与迭代
Set 会维护值插入时的顺序,因此支持按顺序迭代。
集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过values()
方法来取得这个迭代器。
const s = new Set(["val1", "val2", "val3"]);
for (let value of s.values()) {
alert(value);
}
// val1
// val2
// val3
因为values()是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组:
const s = new Set(["val1", "val2", "val3"]);
console.log([...s]); // ["val1", "val2", "val3"]
6.7 WeakSet
WeakSet 是Set 的“兄弟”类型,其API 也是Set 的子集。WeakSet 中的“weak”(弱),描述的是JavaScript 垃圾回收程序对待“弱集合”中值的方式。
6.7.1
可以使用new 关键字实例化一个空的WeakSet:
const ws = new WeakSet();
弱集合中的值只能是Object 或者继承自Object 的类型,尝试使用非对象设置值会抛出TypeError。
如果想在初始化时填充弱集合,则构造函数可以接收一个可迭代对象,其中需要包含有效的值。可迭代对象中的每个值都会按照迭代顺序插入到新实例中:
const val1 = {id: 1},
val2 = {id: 2},
val3 = {id: 3};
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]);
alert(ws1.has(val1)); // true
alert(ws1.has(val2)); // true
alert(ws1.has(val3)); // true
初始化之后可以使用add()
再添加新值,可以使用has()
查询,还可以使用delete()
删除。
弱值性质与不可迭代性质与WeakMap的原理相同。
6.8 迭代与扩展操作
有4 种原生集合类型定义了默认迭代器:
- Array
- 所有定型数组
- Map
- Set
意味着上述所有类型都支持顺序迭代,都可以传入for-of 循环:
for (const iterableThing of iterableThings) {
for (const x of iterableThing) {
console.log(x);
}
}
意味着所有这些类型都兼容扩展操作符。扩展操作符在对可迭代对象执行浅复制时特别有用,只需简单的语法就可以复制整个对象:
let arr1 = [1, 2, 3];
let arr2 = [...arr1];
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3]
console.log(arr1 === arr2); // false
对于期待可迭代对象的构造函数,只要传入一个可迭代对象就可以实现复制:
let map1 = new Map([[1, 2], [3, 4]]);
let map2 = new Map(map1);
console.log(map1); // Map {1 => 2, 3 => 4}
console.log(map2); // Map {1 => 2, 3 => 4}
也可以构建数组的部分元素:
let arr1 = [1, 2, 3];
let arr2 = [0, ...arr1, 4, 5];
console.log(arr2); // [0, 1, 2, 3, 4, 5]
浅复制意味着只会复制对象引用,复制指针,指向的还是同一个对象。
let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = 'bar';
console.log(arr2[0]); // { foo: 'bar' }
6.9 小结
JavaScript 中的对象是引用值,可以通过几种内置引用类型创建特定类型的对象。
- 引用类型与传统面向对象编程语言中的类相似,但实现不同。
- Object 类型是一个基础类型,所有引用类型都从它继承了基本的行为。
- Array 类型表示一组有序的值,并提供了操作和转换值的能力。
- 定型数组包含一套不同的引用类型,用于管理数值在内存中的类型。
- Date 类型提供了关于日期和时间的信息,包括当前日期和时间以及计算。
- RegExp 类型是ECMAScript 支持的正则表达式的接口,提供了大多数基本正则表达式以及一些 高级正则表达式的能力。
JavaScript 比较独特的一点是,函数其实是Function 类型的实例,这意味着函数也是对象。由于函数是对象,因此也就具有能够增强自身行为的方法。
因为原始值包装类型的存在,所以JavaScript 中的原始值可以拥有类似对象的行为。有3 种原始值包装类型:Boolean、Number 和String。它们都具有如下特点。
- 每种包装类型都映射到同名的原始类型。
- 在以读模式访问原始值时,后台会实例化一个原始值包装对象,通过这个对象可以操作数据。
- 涉及原始值的语句只要一执行完毕,包装对象就会立即销毁。
JavaScript 还有两个在一开始执行代码时就存在的内置对象:Global 和Math。其中,Global 对象在大多数ECMAScript 实现中无法直接访问。不过浏览器将Global 实现为window 对象。所有全局变量和函数都是Global 对象的属性。Math 对象包含辅助完成复杂数学计算的属性和方法。
ECMAScript 6 新增了一批引用类型:Map、WeakMap、Set 和WeakSet。这些类型为组织应用程序数据和简化内存管理提供了新能力。