英文:
Swipe values with keys, -> Map vs Object in Javascript
问题
在Map中,它进入了一个无限循环,但在Object中,它进入了一个有限循环。我不知道为什么会发生这种情况。请帮忙!
以下是代码:
const obj = { 1: "one", 2: "two" };
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of map) { // 进入无限循环
map.set(value, key);
map.delete(key);
}
console.log(map); // 您将永远不会得到这个响应
for (const key in obj) { // 进入有限循环
const value = obj[key];
obj[value] = key
delete obj[key];
}
console.log(obj); // 您将获得响应
希望这能帮助你解决问题。
英文:
I was trying to swap keys with values in Map and Object.
In Map it goes in an infinite loop, but in Object it goes in a finite loop. I don't know why is that happening. Please help!
Here is the Code.
const obj = { 1: "one", 2: "two" };
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of map) { // Goes Infinite Loop
map.set(value, key);
map.delete(key);
}
console.log(map); // you will never get this response
for (const key in obj) { // Goes Finite Loop
const value = obj[key];
obj[value] = key
delete obj[key];
}
console.log(obj); // you will get the response
答案1
得分: 1
你在迭代时改变了地图,从而添加了新的键进行迭代。因此,迭代会变得无限循环,因为在每次迭代周期中都会出现新的键进行迭代。要解决这个问题,你需要在突变之前获取原始键的副本:
{
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const key of [...map.keys()]) { // 在这里创建键的副本
map.set(map.get(key), key);
map.delete(key);
}
console.log(JSON.stringify(Object.fromEntries([...map])));
}
// 使用 Map::entries()
{
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of [...map]) { // 在这里创建条目的副本
map.set(value, key);
map.delete(key);
}
console.log(JSON.stringify(Object.fromEntries([...map])));
}
正如我所预期的那样,通过键进行迭代可能更快,因为没有复制所有值的开销。
英文:
You mutate the map while iterating thus adding new keys to iterate. So the iteration goes infinite since a new key appears to iterate each iteration cycle. To solve that you need to get a copy of the original keys ahead of the mutation:
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
{
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const key of [...map.keys()]) { // make a copy of keys here
map.set(map.get(key), key);
map.delete(key);
}
console.log(JSON.stringify(Object.fromEntries([...map])));
}
// using Map::entries()
{
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of [...map]) { // make a copy of entries here
map.set(value, key);
map.delete(key);
}
console.log(JSON.stringify(Object.fromEntries([...map])));
}
<!-- end snippet -->
As I expected the iterating over keys is potentially faster because there's no overhead to copy all values:
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-html -->
<script benchmark data-count="1">
const length = 1000000;
const map = new Map(Array.from({ length }, (_, idx) => [idx + 1, `value${idx + 1}`]));
// @benchmark using keys
for (const key of [...map.keys()]) { // make a copy of keys here
map.set(map.get(key), key);
map.delete(key);
}
// @benchmark using entries
for (const [key, value] of [...map]) { // make a copy of entries here
map.set(value, key);
map.delete(key);
}
// @benchmark using Array.from()
for (const [key, value] of Array.from(map)) {
map.set(value, key);
map.delete(key);
}
</script>
<script src="https://cdn.jsdelivr.net/gh/silentmantra/benchmark/loader.js"></script>
<!-- end snippet -->
答案2
得分: 1
你的第一次尝试导致无限循环的原因是,因为for...of
正在迭代map
的实时内容。如果你添加一个新的键/值对,它将被放置在迭代周期的末尾(类似于array.push(item)
将项目放在末尾)。这会导致无限循环。
Map
中的键以一种简单而直接的方式排序:Map
对象按照条目插入的顺序迭代条目、键和值。- MDN
让我们展开循环,以显示确切发生了什么。
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of map) { // 无限循环
map.set(value, key);
map.delete(key);
}
// 1) key = 1, value = "one"
map.set("one", 1);
map.delete(1); // ▼ 下一个迭代
// map = Map{ [已删除], 2: "two", "one": 1 }
// ▲ 当前迭代
// 2) key = 2, value = "two"
map.set("two", 2);
map.delete(2); // ▼ 下一个迭代
// map = Map{ [已删除], "one": 1, "two": 2 }
// ▲ 当前迭代
// 3) key = "one", value = 1
map.set(1, "one");
map.delete("one"); // ▼ 下一个迭代
// map = Map{ [已删除], "two": 2, 1: "one" }
// ▲ 当前迭代
// 4) key = "two", value = 2
map.set(2, "two");
map.delete("two"); // ▼ 下一个迭代
// map = Map{ [已删除], 1: "one", 2: "two" }
// ▲ 当前迭代
// 5) key = 1, value = "one"
map.set("one", 1);
map.delete(1); // ▼ 下一个迭代
// map = Map{ [已删除], 2: "two", "one": 1 }
// ▲ 当前迭代
// 无限循环 ...
正如你所看到的,迭代永远无法完成,因为你不断在末尾添加新条目。
要解决这个问题,你只需确保你在非实时集合上进行迭代,该集合在更改map
时不会更新。这可以通过将当前内容简单地转换为数组来实现。
for (const [key, value] of Array.from(map)) {
// ...
}
这将创建数组[[1, "one"], [2, "two"]]
并在迭代期间遍历其内容,而在迭代过程中不会更改它。
for...in
是有限的,因为ECMAScript规范要求属性名最多被迭代一次。
迭代器的
next
方法处理对象属性,以确定是否将属性键作为迭代器值返回。返回的属性键不包括符号键。目标对象的属性可能在枚举期间被删除。在迭代期间删除的属性将被忽略。如果在枚举期间向目标对象添加新属性,则不能保证新添加的属性会在活动枚举中被处理。属性名将在任何枚举中最多由迭代器的next
方法返回一次。
然而,这第一个原因并不能完全解释为什么结果是{ one: "1", two: "2" }
。因为如果新添加的属性也被迭代,你最终会得到原始对象{ 1: "one", 2: "two" }
。
你可能已经注意到了规范中我加粗的那一行。
如果在枚举期间向目标对象添加新属性,则不能保证新添加的属性会在活动枚举中被处理。
这意味着结果取决于JavaScript环境。不迭代新属性的环境将产生(期望的)反转对象。迭代新属性的环境将产生非反转对象。
这个行为也在MDN文档中有描述:
删除、添加或修改的属性
如果在一次迭代中修改属性,然后在稍后的时间访问它,那么迭代中的值将是在稍后时间的值。在访问之前被删除的属性将不会在稍后被访问。在迭代期间添加到正在迭代的对象的属性可能会被访问或在迭代中省略。
通常情况下,在迭代期间不要添加、修改或删除对象的属性,除了当前正在访问的属性。无法保证新添加的属性是否会被访问,除了当前正在访问的属性之外,修改的属性(而不是当前的属性)会在修改之前或之后被访问,已删除的属性是否会在删除之前被访问。
在大多数情况下,最好创建一个新的反转Map实例,而不是修改当前实例。
const inverted = new Map(Array.from(map, ([key, value]) => [value, key]));
英文:
The reason why your first attempt results in an infinite loop, is because for...of
is iterating the live contents of map
. If you add a new key/value pair it will be placed at the end of the iteration cycle (similar to how array.push(item)
places the item at the end). This cause an infinite loop.
> The keys in Map
are ordered in a simple, straightforward way: A Map
object iterates entries, keys, and values in the order of entry insertion. - MDN
Let's unroll the loop to show exactly what's going on.
const map = new Map([
[1, "one"],
[2, "two"],
]);
for (const [key, value] of map) { // Goes Infinite Loop
map.set(value, key);
map.delete(key);
}
// 1) key = 1, value = "one"
map.set("one", 1);
map.delete(1); // ▼ next iteration
// map = Map{ [deleted], 2: "two", "one": 1 }
// ▲ current iteration
// 2) key = 2, value = "two"
map.set("two", 2);
map.delete(2); // ▼ next iteration
// map = Map{ [deleted], "one": 1, "two": 2 }
// ▲ current iteration
// 3) key = "one", value = 1
map.set(1, "one");
map.delete("one"); // ▼ next iteration
// map = Map{ [deleted], "two": 2, 1: "one" }
// ▲ current iteration
// 4) key = "two", value = 2
map.set(2, "two");
map.delete("two"); // ▼ next iteration
// map = Map{ [deleted], 1: "one", 2: "two" }
// ▲ current iteration
// 5) key = 1, value = "one"
map.set("one", 1);
map.delete(1); // ▼ next iteration
// map = Map{ [deleted], 2: "two", "one": 1 }
// ▲ current iteration
// infinity ...
Like you can see the iteration can never finish, because you keep adding new entries at the end.
To solve this issue all you have to do is to make sure you're iterating over a non-live collection that doesn't update whenever you change map
. This can be done by simply converting the current contents into an array first.
for (const [key, value] of Array.from(map)) {
// ...
}
This will create the array [[1, "one"], [2, "two"]]
and iterate over its contents, without it changing during iteration.
for...in
is finite because ECMAScript specification requires that property names are at most iterated once.
> The iterator's next
method processes object properties to determine whether the property key should be returned as an iterator value. Returned property keys do not include keys that are Symbols. Properties of the target object may be deleted during enumeration. A property that is deleted before it is processed by the iterator's next
method is ignored. If new properties are added to the target object during enumeration, the newly added properties are not guaranteed to be processed in the active enumeration. A property name will be returned by the iterator's next
method at most once in any enumeration.
However, this first reason doesn't fully explain why the result is { one: "1", two: "2" }
. Because if the newly added properties where also iterated you would end up with the original object { 1: "one", 2: "two" }
.
<!-- begin snippet: js hide: true console: true babel: false -->
<!-- language: lang-js -->
const obj = {
1: "one",
2: "two",
};
for (const key in obj) { // Goes Finite Loop
const value = obj[key];
obj[value] = key
delete obj[key];
}
// 1) key = "1", value = "one"
obj["one"] = "1"
delete obj["1"]; // ▼ next iteration
// obj = { [deleted], 2: "two", one: "1" }
// ▲ current iteration
// 2) key = "2", value = "two"
obj["two"] = "2"
delete obj["2"]; // ▼ next iteration
// obj = { [deleted], one: "1", two: "2" }
// ▲ current iteration
// 3) key = "one", value = "1"
obj["1"] = "one"
delete obj["one"]; // ▼ next iteration
// obj = { 1: "one", [deleted], two: "2" }
// ▲ current iteration
// 4) key = "two", value = "2"
obj["2"] = "two"
delete obj["two"];
// obj = { 1: "one", 2: "two", [deleted] }
// ▲ current iteration
console.log(obj);
// Non-negative integer keys will be traversed first, that's
// why I placed keys 1 and 2 in step 3 and 4 before [deleted].
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in#description
<!-- end snippet -->
You might already have spotted the line above the one I've made bold in the spec.
> If new properties are added to the target object during enumeration, the newly added properties are not guaranteed to be processed in the active enumeration.
Meaning that the outcome is depended on the JavaScript environment. Environments that DO NOT iterate the new properties will yield the (desired) inverted object. Environments that DO iterate the new properties will yield a non-inverted object.
This behaviour is also described in the MDN documentation:
> ### Deleted, added, or modified properties
>
> If a property is modified in one iteration and then visited at a later time, its value in the loop is its value at that later time. A property that is deleted before it has been visited will not be visited later. Properties added to the object over which iteration is occurring may either be visited or omitted from iteration.
>
> In general, it is best not to add, modify, or remove properties from the object during iteration, other than the property currently being visited. There is no guarantee whether an added property will be visited, whether a modified property (other than the current one) will be visited before or after it is modified, or whether a deleted property will be visited before it is deleted.
In most scenarios it's probably better to create a new inverted Map instance, instead of mutating the current instance.
const inverted = new Map(Array.from(map, ([key, value]) => [value, key]));
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论