Map
是 JavaScript 中的一种数据结构,它可以用于存储键值对,并且键可以是任意类型的值。与普通对象相比,Map
提供了更多的灵活性和扩展性。
以下是 Map
的一些特点和用法:
Map
中的键可以是任意类型的值,包括原始类型、对象和函数等。这使得 Map
更加灵活,不仅限于使用字符串作为键。Map
中的元素按照插入顺序维护,并且可以通过迭代器遍历其中的元素。这意味着可以按照插入的顺序来访问和操作 Map
中的键值对。Map
提供了一系列的方法,包括 set()
、get()
、has()
、delete()
等。可以使用这些方法来添加、获取、检查是否包含特定的键值对以及删除指定的键值对。Map
具有 size
属性,用于获取 Map
中键值对的数量。Map
可以使用任意类型的值作为键,这使得 Map
更加灵活和具有更广泛的应用场景。Map
实现了迭代器接口,可以使用 for...of
循环或者 forEach()
方法对 Map
进行遍历。Map
还提供了 WeakMap
类型,它的键只能是对象,并且支持弱引用。这种特性在某些场景下有助于避免内存泄漏Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
上面代码使用 Map 结构的set
方法,将对象o
当作m
的一个键,然后又使用get
方法读取这个键,接着使用delete
方法删除了这个键。
上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
上面代码在新建 Map 实例时,就指定了两个键name
和title
。
Map
构造函数接受数组作为参数,实际上执行的是下面的算法。
const items = [
['name', '张三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map
构造函数的参数。这就是说,Set
和Map
都可以用来生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map
构造函数的参数,结果都生成了新的 Map 对象。
如果对同一个键多次赋值,后面的值将覆盖前面的值
const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
上面代码对键1
连续赋值两次,后一次的值覆盖前一次的值。
如果读取一个未知的键,则返回undefined
。
new Map().get('asfddfsasadf')
// undefined
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set
和get
方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get
方法无法读取该键,返回undefined
。
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
上面代码中,变量k1
和k2
的值是一样的,但是它们在 Map 结构中被视为两个键。
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键。
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
Map 结构的实例有以下属性和操作方法。
(1)size 属性
size
属性返回 Map 结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
(2)Map.prototype.set(key, value)
set
方法设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
const m = new Map();
m.set('edition', 6) // 键是字符串
m.set(262, 'standard') // 键是数值
m.set(undefined, 'nah') // 键是 undefined
set
方法返回的是当前的Map
对象,因此可以采用链式写法
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
(3)Map.prototype.get(key)
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!
(4)Map.prototype.has(key)
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true
(5)Map.prototype.delete(key)
delete()
方法删除某个键,返回true
。如果删除失败,返回false
。
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
(6)Map.prototype.clear()
clear()
方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。需要特别注意的是,Map 的遍历顺序就是插入顺序。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator
属性),就是entries
方法。
map[Symbol.iterator] === map.entries
// true
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...
)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
结合数组的map
方法、filter
方法,可以实现 Map 的遍历和过滤(Map 本身没有map
和filter
方法)。
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
此外,Map 还有一个forEach
方法,与数组的forEach
方法类似,也可以实现遍历。
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...
)。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()
。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
此外,也可以自己实现一个转换函数。
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
WeakMap是JavaScript的一种内置对象,它是一种弱引用的映射表,用于存储键值对。它与普通的Map对象相似,但有一些不同之处。
首先,WeakMap的键只能是对象,而不能是原始数据类型(如字符串、数字等)。这是因为WeakMap使用对象的引用作为键,而不是对象的值。这也意味着当没有其他引用指向该键时,该键会被自动回收,从而避免了内存泄漏。
其次,WeakMap中的键是弱引用的,这意味着如果没有其他引用指向键对象,键对象会被垃圾回收。这样可以确保WeakMap只保存那些仍然可访问的键值对,从而减少内存占用。
WeakMap具有以下特点:
WeakMap常用于需要将额外数据与对象关联起来,但又不想影响对象本身的场景。例如,可以使用WeakMap来存储私有数据,这样可以确保私有数据不会被外部访问到,并且在对象被垃圾回收时,私有数据也会被自动清除。
需要注意的是,WeakMap并不是完全替代Map的数据结构,它们各自适用于不同的场景。如果需要在代码中使用键值对,并且不需要考虑内存泄漏的问题,可以选择使用Map。而如果需要在对象间建立一种弱关联,并希望在对象被销毁时自动清除关联数据,可以选择使用WeakMap。