何时使用地图而不是普通的 JavaScript 对象
普通的 JavaScript Object
{ key: 'value' } 保存结构化数据。但是一个普通的 JavaScript 对象有它的局限性:
只有字符串和符号可以用作对象的键。如果我们使用任何其他东西,比如数字作为对象的键,那么在访问这些键期间,我们将看到这些键将被隐式转换为字符串,从而导致我们失去类型的一致性。常量名称= {1:'一个',2:'两个'};对象.键(名称);// ['1', '2']
通过将 JavaScript 标识符写入对象的键名(例如,toString、构造函数等),可能会意外覆盖从原型继承的属性
另一个对象不能用作对象的键,因此不能通过将该对象写入另一个对象的键来为该对象写入额外的信息,而另一个对象的值将包含额外的信息
对象不是迭代器
无法直接确定对象的大小
Maps 解决了 Objects 的这些限制,但我们必须将 Maps 视为 Objects 的补充而不是替代。基本上 Map 只是数组数组,但我们必须将该数组数组作为带有 new 关键字的参数传递给 Map 对象,否则仅对于数组数组,Map 的有用属性和方法不可用。并记住数组数组或 Map 中的键值对必须仅用逗号分隔,而不能像普通对象中那样使用冒号。
决定使用地图还是对象的三个技巧
当键在运行时之前未知时,对对象使用映射,因为如果这些键覆盖了对象的继承属性,则由用户输入或在不知不觉中形成的键会破坏使用该对象的代码,因此在这些情况下映射更安全。当所有键都是相同类型并且所有映射都是相同类型时,也要使用映射。
如果需要将原始值存储为键,请使用映射。
如果我们需要对单个元素进行操作,请使用对象。
使用地图的好处
1. Map 接受任何键类型并保留键的类型:
我们知道,如果对象的键不是字符串或符号,那么 JavaScript 会隐式地将其转换为字符串。相反,Map 接受任何类型的键:字符串、数字、布尔值、符号。等等,并且 Map 保留原始密钥类型。在这里,我们将使用数字作为 Map 中的键,它仍然是一个数字:
const numbersMap= new Map();
numbersMap.set(1, 'one');
numbersMap.set(2, 'two');
const keysOfMap= [...numbersMap.keys()];
console.log(keysOfMap); // [1, 2]
在 Map 中,我们甚至可以使用整个对象作为键。有时我们可能想要存储一些与对象相关的数据,而不是将这些数据附加到对象本身中,以便我们可以处理精简对象但想要存储有关对象的一些信息。在这些情况下,我们需要使用 Map,以便我们可以将 Object 作为 key,将 object 的相关数据作为 value。
const foo= {name: foo};
const bar= {name: bar};
const kindOfMap= [[foo, 'Foo related data'], [bar, 'Bar related data']];
但是这种方法的缺点是按键访问值的复杂性,因为我们必须遍历整个数组才能获得所需的值。
function getBy Key(kindOfMap, key) {
for (const [k, v] of kindOfMap) {
if(key === k) {
return v;
}
}
return undefined;
}
getByKey(kindOfMap, foo); // 'Foo related data'
我们可以通过使用适当的 Map 来解决无法直接访问该值的问题。
const foo= {name: 'foo'};
const bar= {name: 'bar'};
const myMap= new Map();
myMap.set(foo, 'Foo related data');
myMap.set(bar, 'Bar related data');
console.log(myMap.get(foo)); // 'Foo related data'
我们可以使用 WeakMap 来做到这一点,只需编写 const myMap= new WeakMap()。Map 和 WeakMap 之间的区别在于 WeakMap 允许对键(这里是对象)进行垃圾收集,因此它可以防止内存泄漏,WeakMap 只接受对象作为键,并且 WeakMap 减少了方法集。
2. Map 对键名没有限制:
对于纯 JavaScript 对象,我们可能会意外覆盖从原型继承的属性,这可能很危险。这里我们将覆盖actor对象的toString()属性:
const actor= {
name: 'Harrison Ford',
toString: 'Actor: Harrison Ford'
};
现在让我们定义一个函数 isPlainObject() 来确定提供的参数是否是一个普通对象,这个函数使用 toString() 方法来检查它:
function isPlainObject(value) {
return value.toString() === '[object Object]';
}
isPlainObject(actor); // TypeError : value.toString is not a function
// this is because inside actor object toString property is a string instead of inherited method from prototype
Map 对键名没有任何限制。我们可以在这里使用toString、constructor等键名。虽然actorMap对象有一个名为toString的属性,但是继承自actorMap对象原型的toString()方法可以完美运行。
const actorMap= new Map();
actorMap.set('name', 'Harrison Ford');
actorMap.set('toString', 'Actor: Harrison Ford');
function isMap(value) {
return value.toString() === '[object Map]';
}
console.log(isMap(actorMap)); // true
如果我们遇到用户输入创建键的情况,那么我们必须将这些键放入 Map 而不是普通对象。这是因为用户可以选择自定义字段名称,例如 toString、构造函数等。然后,普通对象中的此类键名可能会破坏以后使用该对象的代码。所以正确的解决方案是将用户界面状态绑定到一个地图,没有办法打破地图:
const userCustomFieldsMap= new Map([['color', 'blue'], ['size', 'medium'], ['toString', 'A blue box']]);
3.地图是可迭代的:
要迭代一个普通对象的属性,我们需要 Object.entries() 或 Object.keys()。Object.entries(plainObject) 返回从对象中提取的键值对数组,然后我们可以解构这些键和值,并可以获得正常的键和值输出。
const colorHex= {
'white': '#FFFFFF',
'black': '#000000'
}
for(const [color, hex] of Object.entries(colorHex)) {
console.log(color, hex);
}
//
'white' '#FFFFFF'
'black' '#000000'
由于 Map 是可迭代的,这就是为什么我们不需要 entry() 方法来迭代 Map 和解构键,值数组可以直接在 Map 上完成,就像在 Map 内部一样,每个元素都以逗号分隔的键值对数组的形式存在.
const colorHexMap = new Map();
colorHexMap.set('white', '#FFFFFF');
colorHexMap.set('black', '#000000');
for(const [color, hex] of colorHexMap) {
console.log(color, hex);
}
//'white' '#FFFFFF' 'black' '#000000'
map.keys ()还返回一个对键的迭代器,而map.values()返回一个对值的迭代器。
4. 我们可以很容易地知道一个 Map 的大小
我们无法直接确定普通对象中的属性数量。我们需要一个像 Object.keys() 这样的辅助函数,它返回一个包含对象键的数组,然后使用长度属性我们可以获得键的数量或普通对象的大小。
const exams= {'John Rambo': '80%', 'James Bond': '60%'};
const sizeOfObj= Object.keys(exams).length;
console.log(sizeOfObj); // 2
但是在 Maps 的情况下,我们可以使用map.size属性直接访问 Map 的大小。
const examsMap = new Map([['John Rambo', '80%'], ['James Bond', '60%']]);
console.log(examsMap.size);