Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 98 additions & 98 deletions 1-js/05-data-types/07-map-set/article.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@

# Map and Set(映射和集合)

我们已经了解过以下复杂数据结构:
我们已经了解了以下复杂的数据结构:

- 存储键集合的对象.
- 存储有序集合的数组.
- 存储键集合的对象
- 存储有序集合的数组

但在我们的开发中并它不能满足需求。这就是为什么 `Map` 和 `Set` 也存在的原因.
但现实生活中远不止这些。这就是为什么 `Map` 和 `Set` 也存在的原因

## Map

[Map](mdn:js/Map) 是一个含有数据的键的集合, 跟普通的 `Object` 一样. 但是它们最大的差别是 `Map` 允许键是任何类型
[Map](mdn:js/Map) 是一个带键的数据集合,跟普通的 `Object` 一样 但是它们最大的差别是 `Map` 允许键(key)是任何类型

它的方法和属性如下:
它的方法和属性如下

- `new Map()` - 创建一个空集合
- `map.set(key, value)` - 存储含有值的键
- `map.get(key)` - 根据键来返回值, 如果 `key` 不在 `map` 里将会返回 `undefined`。
- `map.has(key)` - 如果 `key` 存在则返回 `true`, 否则返回 `false`。
- `map.delete(key)` - 根据键来删除值。
- `map.clear()` - 清空集合。
- `map.size` - 返回当前全部元素的数量
- `new Map()` - 创建 `Map`
- `map.set(key, value)` - 存储键值对
- `map.get(key)` 根据键来返回值如果 `key` 不在 `map` 里将会返回 `undefined`。
- `map.has(key)` 如果 `key` 存在则返回 `true`否则返回 `false`。
- `map.delete(key)` 根据键来删除值。
- `map.clear()` 清空集合。
- `map.size` - 返回当前的元素个数

举个例子:
举个例子

```js run
let map = new Map();
Expand All @@ -31,80 +31,79 @@ map.set('1', 'str1'); // 字符串键
map.set(1, 'num1'); // 数字键
map.set(true, 'bool1'); // 布尔值键

// 还记得常规对象吗? 它会把键转化为字符串
// Map 会保持类型, 所以下面这两个结果不同:
// 还记得普通的 Object 对象吗? 它会将键转化为字符串
// Map 则会保留类型,所以下面这两个结果不同
alert( map.get(1) ); // 'num1'
alert( map.get('1') ); // 'str1'

alert( map.size ); // 3
```

如我们所见,与对象不同,键(key)不会转换为字符串。键可以是任何类型。
如我们所见,与对象不同,键不会转换为字符串。键可以是任何类型。

**Map 还可以使用对象作为键**

例如:
例如

```js run
let john = { name: "John" };

// 我们先存储每个游客的来访次数
// 存储每个用户的来访次数
let visitsCountMap = new Map();

// john 已经在集合里了
// john 是集合中的键
visitsCountMap.set(john, 123);

alert( visitsCountMap.get(john) ); // 123
```

使用对象作为键是 `Map` 最值得注意和重要的功能之一。对于字符串键,`Object`(普通对象)能正常使用,但对于对象键则不能。

我们来尝试一下:
我们来尝试一下

```js run
let john = { name: "John" };

let visitsCountObj = {}; // try to use an object
let visitsCountObj = {}; // 尝试使用对象

visitsCountObj[john] = 123; // try to use john object as the key
visitsCountObj[john] = 123; // 尝试将 john 对象作为键

*!*
// That's what got written!
// 是写成了这样!
alert( visitsCountObj["[object Object]"] ); // 123
*/!*
```

`visitsCountObj` 是一个普通对象的时候, 它会转化所有的键, `john` 被转化为字符串, 所以我们得到字符键 `"[object Object]"`. 很明显这不是我们要的结果.
因为 `visitsCountObj` 是一个普通对象,它会将所有的键如 `john` 转换为字符串,所以我们得到字符键 `"[object Object]"`。这显然不是我们想要的结果。
Comment thread
KerbalHao marked this conversation as resolved.

```smart header="`Map` 是怎么比较键的?"

为了测试键的一致性, `Map` 使用 [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero)算法。 它大致上使用严格全等 `===`, 但不同的是 `NaN` 被看成是等于 `NaN`。 所以 `NaN` 也可以被用作键.
`Map` 使用 [SameValueZero](https://tc39.github.io/ecma262/#sec-samevaluezero)算法来比较键是否相等。 它和严格等于 `===` 差不多,但区别是 `NaN` 被看成是等于 `NaN`。 所以 `NaN` 也可以被用作键

这个算法不能被改变或者自定义。

```

````smart header="链式调用"

每一次调用 `map.set` 都会返回集合本身, 所以我们可以使用:
每一次调用 `map.set` 都会返回集合本身,所以我们可以”连续“调用:

```js
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
```
````


## `Map` 迭代

如果要在 `map` 里使用循环, 可以使用以下三个方法:
如果要在 `map` 里使用循环可以使用以下三个方法

- `map.keys()` - 返回键名的遍历器,
- `map.values()` - 返回键值的遍历器,
- `map.entries()` - 返回实体 `[key, value]` 的遍历器,默认在 `for..of` 中使用。
- `map.keys()` 返回键名的遍历器,
- `map.values()` 返回键值的遍历器,
- `map.entries()` 返回实体 `[key, value]` 的遍历器,默认在 `for..of` 中使用。

例如:
例如

```js run
let recipeMap = new Map([
Expand All @@ -118,34 +117,34 @@ for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}

// 迭代值 (amounts)
// 迭代值amounts
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}

// 迭代 [key, value] 对
for (let entry of recipeMap) { // 效果跟 recipeMap.entries() 相同
alert(entry); // cucumber,500 (and so on)
alert(entry); // cucumber,500(以此类推)
}
```

```smart header="按顺序插入"
```smart header="使用插入顺序"

迭代的顺序与插入键的顺序相同。 `Map` 会保持相同的顺序,不像普通 `Object` 不保证顺序.
迭代的顺序与键的插入顺序相同。不同于普通的 `Object`,`Map` 会保留键的插入顺序。
```

除此之外, `Map` 有个内建 `forEach` 方法, 跟 `Array` 一样:
除此之外`Map` 有个内置的 `forEach` 方法跟 `Array` 类似:

```js
// 对每个 (key, value) 对运行 forEach 函数
// 对每个 (key, value) 键值对运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
alert(`${key}: ${value}`); // cucumber: 500
});
```

## Object.entries: 把对象转化为 `map`
## Object.entries把对象转化为 `map`

`Map` 被创建之后, 我们可以传入带有键值对的数组 (或其它可迭代的对象) 来进行初始化, 像这样:
创建 `Map` 后,我们可以传入带有键值对的数组 (或其它可迭代的对象) 来进行初始化像这样

```js run
// 包含 [key, value] 对的数组
Expand All @@ -158,7 +157,7 @@ let map = new Map([
alert( map.get('1') ); // str1
```

如果我们有个纯对象, 并且想利用这个纯对象来创建 `Map`, 可以使用内建方法 [Object.entries(obj)](mdn:js/Object/entries),它返回一个具有相同格式的并且带有键值对的数组对象
如果我们想从一个已有的 plain object 来创建 `Map`,可以使用内置方法 [Object.entries(obj)](mdn:js/Object/entries),它返回一个对象的键值对数组,该数组符合 `Map` 集合的格式

所以可以像下面这样利用一个对象来创建 `map`

Expand All @@ -180,9 +179,9 @@ alert( map.get('name') ); // John

## Object.fromEntries:把 `map` 转化为对象

我们刚刚已经利用 `Object.entries(obj)` 把一个纯对象转化成 `Map`
我们刚刚已经利用 `Object.entries(obj)` 把一个 plain object 转化成 `Map`

`Object.fromEntries` 方法的作用是相反的: 给定一个具有 `[key, value]` 对的数组, 它会根据给定数组生成对象:
`Object.fromEntries` 方法的作用是相反的给定一个具有 `[key, value]` 对的数组它会根据给定数组生成对象

```js run
let prices = Object.fromEntries([
Expand All @@ -196,11 +195,11 @@ let prices = Object.fromEntries([
alert(prices.orange); // 2
```

我们可以使用 `Object.fromEntries` 从 `Map` 中得到一个纯对象
我们可以使用 `Object.fromEntries` 从 `Map` 中得到一个 plain object

例如, 我们存了数据在 `Map` 中, 但是我们需要把它转给需要纯对象的第三方代码
例如,我们存了数据在 `Map` 中,但是我们需要把它传给需要 plain object 的第三方代码

我们来开始:
我们来开始

```js run
let map = new Map();
Expand All @@ -209,43 +208,43 @@ map.set('orange', 2);
map.set('meat', 4);

*!*
let obj = Object.fromEntries(map.entries()); // make a plain object (*)
let obj = Object.fromEntries(map.entries()); // 生成一个 plain object (*)
*/!*

// done!
// 完成!
// obj = { banana: 1, orange: 2, meat: 4 }

alert(obj.orange); // 2
```

调用 `map.entries()` 将返回含有键值对的数组, 这刚好是 `Object.fromEntries` 所需要的格式.
调用 `map.entries()` 将返回含有键值对的数组这刚好是 `Object.fromEntries` 所需要的格式

我们可以把带 `(*)` 这一行变得更短:
我们可以把带 `(*)` 的这一行写得更短:

```js
let obj = Object.fromEntries(map); // omit .entries()
let obj = Object.fromEntries(map); // 省掉 .entries()
```

上面的代码作用也是一样的, 因为 `Object.fromEntries` 需要一个可迭代对象作为参数, 而一定是数组. `map` 的标准迭代会返回跟 `map.entries()` 一样的键值对. 所以我们可以获得一个与 `Map` 一样具有键值对的纯对象
上面的代码作用也是一样的因为 `Object.fromEntries` 需要一个可迭代对象作为参数,而不一定是数组。`map` 的标准迭代会返回跟 `map.entries()` 一样的键值对所以我们可以获得一个与 `map` 一样具有键值对的 plain object

## Set

`Set` 是一个特别的类型集合 - "值的集合" (没有键 ),它的每一个值只出现一次。
`Set` 是一个特殊类型的集合 - ”值的集合 (没有键 ),它的每一个值只出现一次。

它的主要方法如下:
它的主要方法如下

- `new Set(iterable)` - 创建一个 `set`, 如果提供一个 `iterable` 对象(通常是数组),将会从数组里面复制值到 `set` 里面去
- `set.add(value)` - 添加一个值,返回 set 本身
- `set.delete(value)` - 删除值, 如果 `value` 在调用的时候值存在则返回 `true` ,否则返回 `false`。
- `set.has(value)` - 如果 `value` 存在 set 里面则返回 `true`, 否则返回 `false`。
- `set.clear()` - 移除 set 里面的所有成员
- `set.size` - 返回元素数量
- `new Set(iterable)` 创建一个 `set`,如果提供了一个 `iterable` 对象(通常是数组),将会从数组里面复制值到 `set`
- `set.add(value)` 添加一个值,返回 set 本身
- `set.delete(value)` 删除值如果 `value` 在调用的时候存在则返回 `true` ,否则返回 `false`。
- `set.has(value)` 如果 `value` set 中,返回 `true`否则返回 `false`。
- `set.clear()` - 清空集合
- `set.size` - 返回元素的个数

它的主要特点是重复使用同一个值调用 `set.add(value)` 并不会发生什么改变。 这就是 `Set` 里面的每一个值只出现一次的原因。
它的主要特点是重复使用同一个值调用 `set.add(value)` 并不会发生什么改变。这就是 `Set` 里面的每一个值只出现一次的原因。

例如, 我们有游客来访, 需要记住他们每一个人. 但是已经访问过的旅客会导致重复记录. 每个游客必须只被 "counted" 一次.
例如,我们有客人来访,想记住他们每一个人。但是已经访问过的客人不应重复记录。每个访客必须只被“计数”一次。

`Set` 可以帮助我们解决这个问题:
`Set` 可以帮助我们解决这个问题

```js run
let set = new Set();
Expand All @@ -254,75 +253,76 @@ let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

// visits, 一些访客来访好几次
// visits一些访客来访好几次
set.add(john);
set.add(pete);
set.add(mary);
set.add(john);
set.add(mary);

// set 只保留那个游客第一次来的次数
// set 只保留单一值
alert( set.size ); // 3

for (let user of set) {
alert(user.name); // John (然后 Pete 最后是 Mary)
alert(user.name); // John (然后 Pete Mary)
}
```

使用 `Set` 的场景可以是一个用户数组, 并且每次插入的时候检查重复的的代码也可以使用 [arr.find](mdn:js/Array/find)。但是这样会是性能变的更差,因为这个方法会遍历整个数组来检查每个元素。 `Set` 有更好的内部优化 - 独一无二的检查.
替代 `Set` 的场景可以是一个用户数组,[arr.find](mdn:js/Array/find) 在每次插入时检查是否重复。但是这样会使性能变差,因为这个方法会遍历整个数组来检查每个元素。`Set` 内部对唯一性检查进行了更好的优化。

## Set 迭代

我们可以在 `Set` 中使用 `for..of``forEach` 它们两者之一来循环 :
我们可以使用 `for..of``forEach` 来循环 `Set` 集合:

```js run
let set = new Set(["oranges", "apples", "bananas"]);

for (let value of set) alert(value);

// 跟 forEach 方法相同:
// 跟 forEach 方法相同
set.forEach((value, valueAgain, set) => {
alert(value);
});
```

注意到一件有趣的事情。`forEach` 的回调函数有三个参数:一个 `value`,然后是 *相同值* `valueAgain`,最后才是目标对象本身,的确,相同的值在参数里出现了两次。
注意一件有趣的事情。`forEach` 的回调函数有三个参数:一个 `value`,然后是*相同值* `valueAgain`,最后才是目标对象。没错,相同的值在参数里出现了两次。

那是为了兼容 `Map` 在回调里传入 `forEach` 函数后有三个参数。 当然这看起来有点奇怪。但是这对在特定情况下比如使用 `Set` 代替 `Map`的时候有帮助,反之亦然。
这是为了兼容 `Map` 在回调里传入 `forEach` 函数后有三个参数。当然这看起来有点奇怪。但是这对在特定情况下比如使用 `Set` 代替 `Map` 的时候有帮助,反之亦然。

类似于 `Map`,在 `Set`里用于迭代的方法也被支持:
- `set.keys()` - 返回一个包含值的可迭代对象,
- `set.values()` - 跟 `set.keys()` 作用相同, 为了兼容 `Map`,
- `set.entries()` - 返回一个包含 `[value, value]` 对的可迭代对象 , 它的存在也是为了兼容 `Map`.
`Map` 中用于迭代的方法在 `Set` 中也同样支持:

- `set.keys()` - 返回一个包含值的可迭代对象,
- `set.values()` - 跟 `set.keys()` 作用相同,为了兼容 `Map`,
- `set.entries()` - 返回一个包含 `[value, value]` 对的可迭代对象,它的存在也是为了兼容 `Map`。

## 总结

`Map` - 是一个键值集合.
`Map` - 是一个键值对集合。

方法和属性如下:
方法和属性如下

- `new Map([iterable])` - 创建空的 map, 可选的带有 `[key,value]` 对的`iterable` (例如数组) 对象来进行初始化 。
- `map.set(key, value)` - 存储对应的键值。
- `map.get(key)` - 根据键来返回值, 如果键不存在 map 里就返回 `undefined`。
- `map.has(key)` - 如果 `key` 存在则返回 `true` , 否则返回 `false`。
- `map.delete(key)` - 删除指定键值
- `map.clear()` - 清空 map 。
- `map.size` - 返回当前全部元素的数量
- `new Map([iterable])` 创建空的 map,可选择带有 `[key,value]` 对的 `iterable`例如数组对象来进行初始化 。
- `map.set(key, value)` 存储对应的键值。
- `map.get(key)` 根据键来返回值如果键不存在 map 里就返回 `undefined`。
- `map.has(key)` 如果 `key` 存在则返回 `true`否则返回 `false`。
- `map.delete(key)` - 删除指定键的值
- `map.clear()` 清空 map 。
- `map.size` - 返回当前全部元素的个数

跟普通对象 `Object` 最大的不同点是:
跟 plain object `Object` 最大的不同点是

- 任何键,对象都可以被用作它的键
- 有额外的方法, 和 `size` 属性。
- 任何键、对象都可以作为键
- 有其他的便捷方法,如 `size` 属性。

`Set` - 是一个独一无二的值的集合.
`Set` - 是一组唯一值的集合。

方法和属性:
方法和属性

- `new Set([iterable])` - 创建空的 set , 可选的带有 `iterable` (例如数组) 对象来进行初始化。
- `set.add(value)` - 添加一个 value(如果存在则什么也不做), 返回 set 本身。
- `set.delete(value)` - 删除 value , 如果在调用的时候存在则返回 `true`, 否则返回 `false`。
- `set.has(value)` - 如果则返回 `true`, 否则返回 `false`。
- `set.clear()` - 清空 set
- `set.size` - 返回当前全部元素的数量
- `new Set([iterable])` 创建空的 set,可选的 `iterable`例如数组对象来进行初始化。
- `set.add(value)` - 添加一个值(如果值不存在),返回 set 本身。
- `set.delete(value)` - 删除值,如果 `value` 在调用的时候存在则返回 `true`否则返回 `false`。
- `set.has(value)` - 如果值在集合中,则返回 `true`否则返回 `false`。
- `set.clear()` - 清空集合
- `set.size` - 返回当前全部元素的个数

在 `Map` 和 `Set` 里迭代总是按照插入的顺序来执行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其顺序来获取元素。
在 `Map` 和 `Set` 中迭代总是按照插入的顺序来执行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其顺序来获取元素。