第6章 集合引用类型
对象
数组与定型数组
Map、WeakMap、Set 以及 WeakSet 类型
1. object
很适合存储和在应用程序间交换数据。
显式创建object的两种方式:
(1)new操作符
let person = new Object();
person.name = "Nicholas";
person.age = 29;
(2)对象字面量即对象定义的简写方式,属性名可以是字符串或数值。下面的例子会得到一个带有属性 name、age 和 5 的对象。注意,数值属性会自动转换为字符串。
let person = {name: "Nicholas",age: 29,5: true
};
let person = {}; // 与 new Object()相同
person.name = "Nicholas";
person.age = 29;
开发时多采用对象字面量表示法:
function displayInfo(args) {let output = "";// 用typeof测试每个属性是否存在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"
});
可使用点语法或者中括号来存取对象属性,使用中括号时要用字符串形式:
console.log(person["name"]); // "Nicholas"
console.log(person.name); // "Nicholas"
2. Array(面试重点:构建、判断)
ECMAScript 数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。这意味着可以创建一个数组,它的第一个元素是字符串,第二个元素是数值,第三个是对象。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
(1)创建数组
方式1:使用Array构造函数
let colors1 = new Array();
let colors2 = new Array(20);
let colors3 = new Array("red", "blue", "green");// 也可省略new操作符
let colors = Array(3); // 创建一个包含 3 个元素的数组
let names = Array("Greg"); // 创建一个只包含一个元素,即字符串"Greg"的数组
方式2:使用数组字面量表示法
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个元素的数组
let names = []; // 创建一个空数组
let values = [1,2,]; // 创建一个包含 2 个元素的数组
Array 构造函数还有两个 ES6 新增的用于创建数组的静态方法:from()和 of()。
方式3: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.from()对现有数组执行浅复制
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1);
console.log(a1); // [1, 2, 3, 4]
alert(a1 === a2); // false
// 可以使用任何可迭代对象
const iter = {*[Symbol.iterator]() {yield 1;yield 2;yield 3;yield 4;}
};
console.log(Array.from(iter)); // [1, 2, 3, 4]
// arguments 对象可以被轻松地转换为数组
function getArgsArray() {return Array.from(arguments);
}
console.log(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]
// from()也能转换带有必要属性的自定义对象
const arrayLikeObject = {0: 1,1: 2,2: 3,3: 4,length: 4
};
console.log(Array.from(arrayLikeObject)); // [1, 2, 3, 4]
Array.from()还接收第二个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像调用 Array.from().map()那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函数中 this 的值。但这个重写的 this 值在箭头函数中不适用。
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
const a3 = Array.from(a1, function(x) {return x**this.exponent}, {exponent: 2});
console.log(a2); // [1, 4, 9, 16]
console.log(a3); // [1, 4, 9, 16]
方式4: of()用于将一组参数转换为数组实例,这个方法用于替代在 ES6 之前常用的 Array.prototype.slice.call(arguments),一种异常笨拙的将 arguments 对象转换为数组的写法:
console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined)); // [undefined]
(2)数组空位
方式1:使用一串逗号
const options = [,,,,,]; // 创建包含 5 个元素的数组
console.log(options.length); // 5
console.log(options); // [,,,,,]
ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined:
方式2:利用ES6的Array.from()+一串逗号
const options = [1,,,,5];
for (const option of options) {console.log(option === undefined);
}
// false
// true
// true
// true
// falseconst a = Array.from([,,,]); // 使用 ES6 的 Array.from()创建的包含 3 个空位的数组
for (const val of a) {alert(val === undefined);
}
// true
// true
// true
alert(Array.of(...[,,,])); // [undefined, undefined, undefined]
for (const [index, value] of options.entries()) {alert(value);
}
// 1
// undefined
// undefined
// undefined
// 5
(3)数组索引
let array = []
alert(array.length); // 0
数组 length 属性的独特之处在于,它不是只读的。通过修改 length 属性,可以从数组末尾删除或添加元素。
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors.length = 2;
alert(colors[2]); // undefined,这里相当于删除了末元素let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors.length = 4;
alert(colors[3]); // undefined,这里相当于增加了一个元素let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
colors[99] = "black"; // 添加一种颜色(位置 99)
alert(colors.length); // 100
(4)检测数组(面试题)
方式1:instanceof
if (value instanceof Array){// 操作数组
}
使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
方式2: Array.isArray()
确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。
if (Array.isArray(value)){// 操作数组
}
(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) 复制和填充方法
ES6 新增了两个方法:批量复制方法 copyWithin(),以及填充数组方法 fill()。
(1)fill() 填充数组
使用 fill()方法可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充的位置,它是可选的。如果不提供结束索引,则一直填充到数组末尾。负值索引从数组末尾开始计算。也可以将负索引想象成数组长度加上它得到的一个正索引:
参数:填充的数字(必须) 填充开始索引(可选,默认为0) 填充结束索引(可选,默认到末尾)(左闭右开,超出范围忽略)
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); // 重置
// 用 8 填充索引大于等于 1 且小于 4 的元素
// (-4 + zeroes.length = 1)
// (-1 + zeroes.length = 4)
zeroes.fill(8, -4, -1);
console.log(zeroes); // [0, 8, 8, 8, 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]
(2)copywith() 批量复制
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]
reset();
// 从 ints 中复制索引 0 开始到索引 3 结束的内容
// 插入到索引 4 开始的位置
ints.copyWithin(4, 0, 3);
alert(ints); // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9]
reset();
// JavaScript 引擎在插值前会完整复制范围内的值
// 因此复制期间不存在重写的风险
ints.copyWithin(2, 0, 6);
alert(ints); // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9]
reset();
// 支持负索引值,与 fill()相对于数组末尾计算正向索引的过程是一样的
ints.copyWithin(-4, -7, -3);
alert(ints); // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6]// copyWithin()静默忽略超出数组边界、零长度及方向相反的索引范围:
let ints,reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
// 索引过低,忽略
ints.copyWithin(1, -15, -12);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset()
// 索引过高,忽略
ints.copyWithin(1, 12, 15);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
// 索引反向,忽略
ints.copyWithin(2, 4, 2);
alert(ints); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
reset();
// 索引部分可用,复制、填充可用部分
ints.copyWithin(4, 7, 10)
alert(ints); // [0, 1, 2, 3, 7, 8, 9, 7, 8, 9];
(7)转换方法
所有对象都有 toLocaleString()、toString()和 valueOf()方法。其中,valueOf()
返回的还是数组本身。而 toString()返回由数组中每个值的等效字符串拼接而成的一个逗号分隔的字符串。也就是说,对数组的每个值都会调用其 toString()方法,以得到最终的字符串。
let colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
alert(colors.toString()); // red,blue,green
alert(colors.valueOf()); // red,blue,green
alert(colors); // red,blue,green
toLocaleString()方法也可能返回跟 toString()和 valueOf()相同的结果,但也不一定。在调用数组的 toLocaleString()方法时,会得到一个逗号分隔的数组值的字符串。它与另外两个方法唯一的区别是,为了得到最终的字符串,会调用数组每个值的 toLocaleString()方法,而不是toString()方法。
let person1 = {toLocaleString() {return "Nikolaos";},toString() {return "Nicholas";}
};
let person2 = {toLocaleString() {return "Grigorios";},toString() {return "Greg";
}
};
let people = [person1, person2];
alert(people); // Nicholas,Greg
alert(people.toString()); // Nicholas,Greg
alert(people.toLocaleString()); // Nikolaos,Grigorios
继承的方法 toLocaleString()以及 toString()都返回数组值的逗号分隔的字符串。如果想使用不同的分隔符,则可以使用 join()方法。join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
(8)栈方法
栈(后进先出)
push():插入,返回数组的最新长度
pop():删除末项,返回被删除的项
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2count = colors.push("black"); // 再推入一项
alert(count); // 3let item = colors.pop(); // 取得最后一项
alert(item); // black
alert(colors.length); // 2
(9) 队列方法
队列(先进先出)
push():末尾添加数据,返回数组最新长度
pop():删除末项,返回被删除的项
shift():删除首项,返回该项,长度-1
unshift():开头添加任意多个值,返回数组新长度
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alert(count); // 2count = colors.push("black"); // 再推入一项
alert(count); // 3let item = colors.shift(); // 取得第一项
alert(item); // red
alert(colors.length); // 2let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2count = colors.unshift("black"); // 再推入一项,此时应该是black,red,green
alert(count); // 3let item = colors.pop(); // 取得最后一项
alert(item); // green
alert(colors.length); // 2
(10)排序方法
reverse():反向排序
sort():排序,默认升序
reverse()和 sort()都返回调用它们的数组的引用。(即改变原数组的值)
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
let values = [0, 1, 5, 10, 15];
values.sort();
alert(values); // 0,1,10,15,5
一开始数组中数值的顺序是正确的,但调用 sort()会按照这些数值的字符串形式重新排序。因此,即使 5 小于 10,但字符串"10"在字符串"5"的前头**(先比较第一位)**,所以 10 还是会排到 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//降序
function compare2(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(compare2);
alert(values); // 15,10,5,1,0
箭头函数写法:
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
(11)操作方法
- concat():给原数组连接新元素,创建新数组。如果传入一个或多个数组,则 concat()会把这些数组的每一项都添加到结果数组。
如果参数不是数组,则直接把它们添加到结果数组末尾。
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
不想打平数组,即不把加入的数组元素抽出来加进去,而是直接将原数组作为新元素加入,可在参数数组上指定一个特殊的符号为false:Symbol.isConcatSpreadable。把这个值设置为 true 可以强制打平类数组对象
let colors = ["red", "green", "blue"];
let newColors = ["black", "brown"];
let moreNewColors = {[Symbol.isConcatSpreadable]: true,length: 2,0: "pink",1: "cyan"
};
newColors[Symbol.isConcatSpreadable] = false;
// 强制不打平数组
let colors2 = colors.concat("yellow", newColors);
// 强制打平类数组对象
let colors3 = colors.concat(moreNewColors);
console.log(colors); // ["red", "green", "blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", ["black", "brown"]]
console.log(colors3); // ["red", "green", "blue", "pink", "cyan"]
- slice():创建一个包含原数组中一个或多个元素的新数组。 参数:开始位置(必须) 结束位置(可选,默认到结尾)(左闭右开)
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
alert(colors2); // green,blue,yellow,purple
alert(colors3); // green,blue,yellow
- splice():在数组中插入元素
删除:2个参数,开始位置 结束位置(左闭右开)
插入:3个参数,开始位置 删除元素数量(一般为0) 插入元素
替换:3个参数,开始位置 删除元素数量 插入任意多个元素
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
alert(colors); // green,blue
alert(removed); // red,只有一个元素的数组
removed = colors.splice(1, 0, "yellow", "orange"); // 在位置 1 插入两个元素
alert(colors); // green,yellow,orange,blue
alert(removed); // 空数组
removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
alert(colors); // green,red,purple,orange,blue
alert(removed); // yellow,只有一个元素的数组
(12)搜索和位置方法
两类搜索:严格相等、断言函数
- 严格相等
ECMAScript 提供了 3 个严格相等的搜索方法:indexOf()、lastIndexOf()和 includes()。其中,前两个方法在所有版本中都可用,而第三个方法是ECMAScript 7 新增的。这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。
indexOf() 和 includes() 方法从数组前头(第一项)开始向后搜索,而 lastIndexOf() 从数组末尾(最后一项)开始向前搜索。
indexOf() 和 lastIndexOf() 都返回要查找的元素在数组中的位置,如果没找到则返回-1。
includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一项时,会使用全等(===)比较,也就是说两项必须严格相等。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
alert(numbers.indexOf(4)); // 3
alert(numbers.lastIndexOf(4)); // 5
alert(numbers.includes(4)); // true
alert(numbers.indexOf(4, 4)); // 5
alert(numbers.lastIndexOf(4, 4)); // 3
alert(numbers.includes(4, 7)); // false
let person = { name: "Nicholas" };
let people = [{ name: "Nicholas" }];
let morePeople = [person];
alert(people.indexOf(person)); // -1
alert(morePeople.indexOf(person)); // 0
alert(people.includes(person)); // false
alert(morePeople.includes(person)); // true
- 断言函数
断言函数的返回值决定了相应索引的元素是否被认为匹配。断言函数接收 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}
alert(people.findIndex((element, index, array) => element.age < 28));
// 0
(13)迭代方法(面试题)
every():对数组每一项都运行传入的函数,如果对每一项函数都返回 true,则这个方法返回 true。
some():对数组每一项都运行传入的函数,如果有一项函数返回 true,则这个方法返回 true。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
alert(everyResult); // false
let someResult = numbers.some((item, index, array) => item > 2);
alert(someResult); // true
filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。适合从数组中帅选满足条件的元素。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
alert(filterResult); // 3,4,5,4,3
forEach():对数组每一项都运行传入的函数,没有返回值。
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});
map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组。
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
(14)归并方法
reduce():从数组第一项遍历到最后一项
// 累加数组所有数值
let values = [1, 2, 3, 4, 5];
let sum = values.reduce((prev, cur, index, array) => prev + cur);
alert(sum); // 15
reduceRight():从最后一项遍历到第一项
let values = [1, 2, 3, 4, 5];
let sum = values.reduceRight(function(prev, cur, index, array){return prev + cur;
});
alert(sum); // 15
3. 定型数组TypedArray(似乎不重要)
定型数组(typed array)是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率。实际上,JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组。
4. Map
键值对存储(ES6新特性)。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。
(1)基本API
使用 new 关键字和 Map 构造函数可以创建一个空映射:
const m = new Map();
// 使用嵌套数组初始化映射
const m1 = new Map([["key1", "val1"],["key2", "val2"],["key3", "val3"]
]);
alert(m1.size); // 3// 使用自定义迭代器初始化映射
const m2 = new Map({[Symbol.iterator]: function*() {yield ["key1", "val1"];yield ["key2", "val2"];yield ["key3", "val3"];}
});
alert(m2.size); // 3// 映射期待的键/值对,无论是否提供
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined
初始化之后,set():添加键值对
get() / has() :查询,get获取键对应的值没有返回undefined,has看键对应的值是否存在返回true/false
size():获取键值对数量
delete() / clear():删除值,delete删除某个键值对,clear删除所有
const m = new Map();
alert(m.has("firstName")); // false
alert(m.get("firstName")); // undefined
alert(m.size); // 0// 连续添加键值对
m.set("firstName", "Matt").set("lastName", "Frisbie");alert(m.has("firstName")); // true
alert(m.get("firstName")); // Matt
alert(m.size); // 2m.delete("firstName"); // 只删除这一个键/值对
alert(m.has("firstName")); // false
alert(m.has("lastName")); // true
alert(m.size); // 1m.clear(); // 清除这个映射实例中的所有键/值对alert(m.has("firstName")); // false
alert(m.has("lastName")); // false
alert(m.size); // 0
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。Map 内部使用 SameValueZero 比较操作(ECMAScript 规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与 Object 类似,映射的值是没有限制的。
const m = new Map();
const functionKey = function() {};
const symbolKey = Symbol();
const objectKey = new Object();
m.set(functionKey, "functionValue");
m.set(symbolKey, "symbolValue");
m.set(objectKey, "objectValue");
alert(m.get(functionKey)); // functionValue
alert(m.get(symbolKey)); // symbolValue
alert(m.get(objectKey)); // objectValue
// SameValueZero 比较意味着独立实例不冲突
alert(m.get(function() {})); // undefined
(2)顺序与迭代
Map实例会维护键值对的插入顺序,但是object不会。
const m = new Map([["key1", "val1"],["key2", "val2"],["key3", "val3"]
]);
alert(m.entries === m[Symbol.iterator]); // true
for (let pair of m.entries()) {alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {alert(pair);
}
// [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
(3)选择Object还是Map
☆☆Map和传统的Object有什么区别呢?(或者说,为什么要出现Map数据结构呢?)
- Object只能使用字符串,数值,Symbol作为键,而Map可以使用任何数据类型作为键
- Map使用了类似于哈希表的算法实现了快速查找和删除操作,所以在操作大量数据时,Map的性能跟好。
- Map对象保证了插入顺序,在遍历Map对象时,不需要担心顺序问题。
- Map对象内置了迭代器,可以使用for-of或者其他迭代器方法进行迭代,使得处理Map对象更加灵活。
- Map提供的操作键值对的api,能够对键值对更简单的操作。
5. WeakMap(面试题)
ES6新增了弱映射。WeakMap 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱映射”中键的方式。这些键不属于正式的引用,不会阻止垃圾回收。
Object 只能使用数值、字符串或符号作为键
弱映射的键只能是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
// 原始值可以先包装成对象再用作键
const stringKey = new String("key1");
const wm3 = new WeakMap([stringKey, "val1"
]);
alert(wm3.get(stringKey)); // "val1"
set():添加键值对
get() / has() 查询
delete() 删除
const wm = new WeakMap();
const key1 = {id: 1},
key2 = {id: 2};
alert(wm.has(key1)); // false
alert(wm.get(key1)); // undefined
wm.set(key1, "Matt").set(key2, "Frisbie");
alert(wm.has(key1)); // true
alert(wm.get(key1)); // Matt
wm.delete(key1); // 只删除这一个键/值对
alert(wm.has(key1)); // false
alert(wm.has(key2)); // true
因为 WeakMap 中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然,也用不着像 clear()这样一次性销毁所有键/值的方法。WeakMap 确实没有这个方法。因为不可被迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问 WeakMap 实例,也没办法看到其中的内容。
☆☆Map和weakMap的区别:
Map和weakMap是ES6出现的数据结构。和object类似,都是可以对键值对的存储。与object的区别就是:
(1)object只能使用字符串,数字,Symbol作为键,而Map可以使用任何数据类型作为键或值;
(2)Map对象保证了插入顺序,在遍历Map对象时,不需要担心顺序问题;
(3)Map对象内置了迭代器,可以使用for-of或者其他迭代器方法进行迭代,使得处理Map对象更加灵活;
(4)Map提供的操作键值对的api,能够对键值对更简单的操作;
(5)Map使用类似哈希表的算法实现了快速查找和删除操作,在大量操作数据时比object更为高效。
weakMap是一种弱引用的数据结构,它和Map的区别:
(1)weakMap只能使用引用类型作为键,当这个键在其他地方没有被引用时,这个键值对将会被回收;
(2)weakMap对象的键值对,任何时候可能被回收,所以没有内置迭代器。
(3)如果你想要作为键的对象可以被垃圾回收,你应该使用 WeakMap。如果你想要可以获取键的列表,你应该使用 Map 而不是 WeakMap。
6. Set
ES6新增。Set 在很多方面都像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的。
(1)基本API
const m = new Set();
初始化:
// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
alert(s1.size); // 3
// 使用自定义迭代器初始化集合
const s2 = new Set({[Symbol.iterator]: function*() {yield "val1";yield "val2";yield "val3";}
});
alert(s2.size); // 3
add():增加值
has():查询是否存在,返回true/false
size:取得元素数量
delete() / clear():删除元素
const s = new Set();
alert(s.has("Matt")); // false
alert(s.size); // 0s.add("Matt").add("Frisbie");alert(s.has("Matt")); // true
alert(s.size); // 2s.delete("Matt");alert(s.has("Matt")); // false
alert(s.has("Frisbie")); // true
alert(s.size); // 1s.clear(); // 销毁集合实例中的所有值alert(s.has("Matt")); // false
alert(s.has("Frisbie")); // false
alert(s.size); // 0
连续添加:
const s = new Set().add("val1");s.add("val2").add("val3");alert(s.size); // 3
和Map一样,Set也可以包含任何数据类型作为值。
const s = new Set();const functionVal = function() {};
const symbolVal = Symbol();
const objectVal = new Object();s.add(functionVal);
s.add(symbolVal);
s.add(objectVal);alert(s.has(functionVal)); // true
alert(s.has(symbolVal)); // true
alert(s.has(objectVal)); // true// SameValueZero 检查意味着独立的实例不会冲突
alert(s.has(function() {})); // false
(2)顺序与迭代
Set 会维护值插入时的顺序,因此支持按顺序迭代。 集合实例可以提供一个迭代器(Iterator),能以插入顺序生成集合内容。可以通过 values()方法及其别名方法 keys()(或者 Symbol.iterator 属性,它引用 values()取得这个迭代器:
const s = new Set(["val1", "val2", "val3"]);
alert(s.values === s[Symbol.iterator]); // true
alert(s.keys === s[Symbol.iterator]); // true
for (let value of s.values()) {alert(value);
}
// val1
// val2
// val3
for (let value of s[Symbol.iterator]()) {alert(value);
}
// val1
// val2
// val3
add():
// 创建一个 Set
let mySet = new Set();// 向 Set 中添加元素
mySet.add('apple');
mySet.add('banana');
mySet.add('orange');// 创建一个迭代器
let iterator = mySet.values();// 使用迭代器按顺序迭代 Set 中的值
for (let item of iterator) {console.log(item);
}
因为 values()是默认迭代器,所以可以直接对集合实例使用扩展操作,把集合转换为数组:
const s = new Set(["val1", "val2", "val3"]);
console.log([...s]); // ["val1", "val2", "val3"]
(3)定义正式集合操作
交、差、并、补
7. WeakSet
ES6新增。WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。WeakSet 中的“weak”(弱),描述的是 JavaScript 垃圾回收程序对待“弱集合”中值的方式。
(1)基本API
可以使用 new 关键字实例化一个空的 WeakSet:
const ws = new WeakSet();
初始化:
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// 初始化是全有或全无的操作
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, "BADVAL", val3]);
// TypeError: Invalid value used in WeakSet
typeof ws2;
// ReferenceError: ws2 is not defined// 原始值可以先包装成对象再用作值
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]);
alert(ws3.has(stringVal)); // true
add():添加新值
has():查询
delete():删除
const ws = new WeakSet();
const val1 = {id: 1},val2 = {id: 2};
alert(ws.has(val1)); // falsews.add(val1).add(val2);alert(ws.has(val1)); // true
alert(ws.has(val2)); // truews.delete(val1); // 只删除这一个值alert(ws.has(val1)); // false
alert(ws.has(val2)); // true// add()方法返回弱集合实例,因此可以把多个操作连缀起来,包括初始化声明:
const val1 = {id: 1},
val2 = {id: 2},
val3 = {id: 3};
const ws = new WeakSet().add(val1);
ws.add(val2).add(val3);alert(ws.has(val1)); // true
alert(ws.has(val2)); // true
alert(ws.has(val3)); // true
WeakSet 中“weak”表示弱集合的值是“弱弱地拿着”的。意思就是,这些值不属于正式的引用,不会阻止垃圾回收。
不可迭代值:因为 WeakSet 中的值任何时候都可能被销毁,所以没必要提供迭代其值的能力。
使用弱集合,给对象打标签:
const disabledElements = new Set();
const loginButton = document.querySelector('#login');
// 通过加入对应集合,给这个节点打上“禁用”标签,不可被垃圾回收
disabledElements.add(loginButton);// 可以被垃圾回收
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');
// 通过加入对应集合,给这个节点打上“禁用”标签
disabledElements.add(loginButton);
☆☆介绍下 Set、Map、WeakSet、WeakMap 的区别
参考答案:
Set
- 成员唯一、无序且不重复
- 键值与键名是一致的(或者说只有键值,没有键名)
- 可以遍历,方法有 add, delete,has
WeakSet
- 成员都是对象
- 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏
- 不能遍历,方法有 add, delete,has
Map
- 本质上是健值对的集合,类似集合
- 可以遍历,方法很多,可以跟各种数据格式转换
WeakMap
- 只接受对象作为健名(null 除外),不接受其他类型的值作为键名
- 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾机制回收,此时键名是无效的
- 不能遍历,方法有 get、set、has、delete
8. 迭代与扩展操作
数组Array、映射Map、集合Set都支持顺序迭代,都可以传入for-of循环
let iterableThings = [Array.of(1, 2),typedArr = Int16Array.of(3, 4),new Map([[5, 6], [7, 8]]),new Set([9, 10])
];
for (const iterableThing of iterableThings) {for (const x of iterableThing) {console.log(x);}
}
// 1
// 2
// 3
// 4
// [5, 6]
// [7, 8]
// 9
// 10
浅复制,意味着只会复制和对象引用
let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = 'bar';
console.log(arr2[0]); // { foo: 'bar' }