Using ECMA6 Set as object key - javascript

Is there a way to use Set as object keys
let x = {}
const a = new Set([3, 5])
x[a] = 1
console.log(x) // >{[object Set]: 1}
const b = new Set([1, 4])
x[b] = 2
console.log(x) // >{[object Set]: 2}
The keys are being overwritten even though the sets are not equal.
Thanks!

No this is not possible because Object keys must be strings or symbols. If you would like to use a Set as a key you can try using a Map. Maps are similar to objects except you can use other objects as keys for a map.
One thing to keep in mind is that you cannot use maps exactly like you use Objects.
This is directly from the Mozilla docs.
The following IS NOT A VALID USE OF A MAP.
let wrongMap = new Map()
wrongMap['bla'] = 'blaa'
wrongMap['bla2'] = 'blaaa2'
console.log(wrongMap) // Map { bla: 'blaa', bla2: 'blaaa2' }
But that way of setting a property does not interact with the Map data structure. It uses the feature of the generic object. The value of 'bla' is not stored in the Map for queries. Other operations on the data fail:
Correct use of a map looks like the below:
let map = new Map()
// setting values
map.set(key, value)
// getting values
map.get(key)
Remember that if you use an Object like a Set as a key, the reference of the Set is what matters.
If you instantiate two sets separately, even if they both have the same contents, they will have different references and be considered different keys.

Do you mean that the map in ES6 like this:
x = new Map()
a = new Set([3, 5])
x.set(a, 1)
console.log(x);

Related

Javascript "Set" for Objects and Arrays

Does Javascript have a built-in type for making a set out of data-objects and arrays?
let set = new Set();
set.add({"language": "ecmascript"});
set.add({"language": "ecmascript"});
set.add({"language": "ecmascript"});
set.add({"language": "ecmascript"});
set.add([1,2,3]);
set.add([1,2,3]);
set.add([1,2,3]);
set.add([1,2,3]);
console.log(set);
The Set I'm using above is only useful for primitives.
The Set I'm using above is only useful for primitives.
That's incorrect, it works just fine for objects. The problem is that distinct objects with the same properties and property values are not equal, so doing set.add({"language": "ecmascript"}); twice adds two non-equal objects to the set (both with the same property name and value).
If you add the same object more than once, it won't be added a second time:
const set = new Set();
const obj = {"language": "ecmascript"};
set.add(obj);
set.add(obj);
console.log(set.size); // 1
Does Javascript have a built-in type for...
If you want objects with the same properties and values to be treated as equal, then no. You'd need to be able to specify a comparison operation, and there's no built-in Set in JavaScript that lets you define the comparison operation to use.
Obviously, you can create one. As a starting point, I'd probably use a Map keyed by the names of the properties on the object, sorted and turned into a string via JSON.stringify. (Although that won't work if you want to have Symbol keys as part of the definition of equality.) For instance, if you're only considering own properties:
const key = JSON.stringify(Object.getOwnPropertyNames(object).sort());
The value for an entry could be either just an array of the objects with those keys that you do a linear search on, or a second Map keyed by some kind of hash of the property values, depending on how many objects you need to handle...
In comments, I asked:
Do you only need to handle objects with JSON-serializable values?
and you answered:
I have a bunch of objects that are already serialized, but there are duplicates that I'd like to eliminate and then re-serialize.
Yeah, you can use a Set for that if you don't mind re-serializing, or a Map if you want to skip the re-serializing part:
const unique = new Map();
for (const source of serializedObjects) {
const sourceObject = JSON.parse(source); // Or parse from whatever serialization it is
// Build object adding properties in alpha order for stability
const keyObj = {};
for (const key of Object.keys(sourceObject).sort()) {
keyObj[key] = sourceObject[key];
}
// Save it using JSON.stringify, which uses ES2015 property order
map.set(JSON.stringify(keyObj), source);
}
const uniqueSourceStrings = [...map.values()];
Or for the de-serialized objects themselves:
const unique = new Map();
for (const source of serializedObjects) {
const sourceObject = JSON.parse(source); // Or parse from whatever serialization it is
// Build object adding properties in alpha order for stability
const keyObj = {};
for (const key of Object.keys(sourceObject).sort()) {
keyObj[key] = sourceObject[key];
}
// Save it using JSON.stringify, which uses ES2015 property order
map.set(JSON.stringify(keyObj), sourceObject); // <=================== changed
}
const uniqueSourceObject = [...map.values()];
// ^^================================================================== changed

How to append values to ES6 Map

I am trying to learn about ES6 Map data structures and am having difficulty understanding some of their behaviour. I would like to create a Map with an Array as a value and append (push) new values onto the current value of the Map. For example:
let m = new Map()
m.set(1, []) // []
m.set(1, m.get(1).push(2)) // [[1, 1]]
I am confused as to why I do not get [2] as the value of m.get(1) above. How can I append values to the array in my map?
That's because the method push returns the size of the array after the insertion.
You can change your code to the following to append to an array:
m.get(1).push(2);
And it'll update the value in the map, there's no need to try to re-set the value again as the value is passed back as reference.
The best way to define a Map according to your need is to explicitly tell the Map, what kind of data you want to deal with.
in your case you want values in an array, we could get using a "string id" for example
In this case you will have this :
let map = new Map<String, Array<any>>
Then you can create items like map["key"] = ["lol", 1, null]
There is two thing. First as #Adriani6 said the push method do not returns a pointer to the array but the size of the array.
Secondly, you do not need to do an other m.set, because your push will affect directly the array behind the reference returned by m.get
function displayMap(m) {
m.forEach(function(val, key) {
console.log(key + " => " + val);
});
}
let m = new Map();
m.set(1, []);
displayMap(m);
m.get(1).push(20);
displayMap(m);
It fails, because the return of push() is the size of the array after push.
You can push the content after doing a get().
m.get(1).push(2);
If you want to test set() then write a a self executable function like this:
let m = new Map()
m.set(1, []) // []
console.log(m.get(1))
m.set(1, (() => {m.get(1).push(2);return m.get(1);})());
console.log(m.get(1))
Here's a working example of what you are trying to do (open console)
Have a look here. As you can see the push method returns the new length of the array you just mutated, hence your result.

How to serialize a Map in javascript?

So...there is this type in js called Map and it is really nice...it is faster than an Object for iterations and calculations so I really like it. However...you can't pass Maps around as you could with objects.
I know that I could turn Map into JSON but it is costly to do so and it kind of looses the point of using Maps in the first place.
JSON format is not meant for Maps...it was meant for Objects.
So...lets move from the JSON format a little.
Is there a way for me to serialize a Map into a string in any way so that I can then do the opposite - from said serialized Map to end up with a Map
Preferably this method should be as easy to perform as JSON.stringify or its counterpart JSON.parse.
I want to use Map as it is faster and better but I need to send my data as string. The format of the string is not important as long as I can parse it back into a Map
-- edit: Added the missing JSON Stringify function during serialization -
There is a native way of doing this in Javascript.
the Map object has a method called entries() that returns an iterator that will yield each entry in the map as an array with 2 values in it. It will look like [key, value].
To avoid writing any looping code yourself, you can use Array.from() which can consume an iterator and extract each item from it. Having that, the following will give you your Map object in a serialized form.
let mySerialMap = JSON.stringify(Array.from(myMap.entries()))
console.log(mySerialMap)
Now, that's not even the coolest part. But before getting there, let me show you one possible way of using the Map constructor.
let original = new Map([
['key1', 'the-one-value']
['second-key, 'value-two']
])
The array of array that you can see being passed to the Map object is in the same format as what you get from using Array.from(myMap.entries()).
So you can rebuild you map in a single line using the following sample code:
let myMap = new Map(JSON.parse(mySerialMap))
And you can use myMap as you would any Map.
let myMap = new Map(JSON.parse(mySerialMap))
let first = myMap.get('some-key')
myMap.set('another-key', 'some value')
I guess the whole point of Maps/Dictionaries is that you can use objects as keys in them, so:
let a = {}, b = {}, m = new Map();
m.set(a,b);
m.get(a); // b
So you get b since you have a reference on a. Let's say you serialize the Map by creating an Array of arrays, and stringify that to json:
function serialize (map) {
return JSON.stringify([...map.entries()])
}
let s = serialize(m); // '[[{}, {}]]'
// '[[<key>, <val>], … ]'
Than you could:
let m2 = JSON.parse(s).reduce((m, [key, val])=> m.set(key, val) , new Map());
But the question now is: How to get a certain key? Since there does not exist any reference, due to the fact that all objects have been stringified and recreated, it is not possible to query a dedicated key.
So all that would only work for String keys, but that really takes most of power of maps, or in other words, reduces them to simple objects, what is the reason maps were implemented.
To #philipp's point, people who care about the serialization of the Map will probably prefer objects (leverages intuition, reduces '[]' arithmetic). Object.entries() and Object.fromEntries() can make that a bit more literate:
writeMe = new Map()
writeMe.set('a', [1])
writeMe.set('b', {myObjValue: 2})
// ▶ Map(2) {'a' => Array(1), 'b' => {…}}
written = JSON.stringify(Object.fromEntries(writeMe.entries()))
// '{"a":[1],"b":{"myObjValue":2}}'
read = new Map(Object.entries(JSON.parse(written)))
// ▶ Map(2) {'a' => Array(1), 'b' => {…}}
read.get("b")
// ▶ {myObjValue: 2}

How to convert Map keys to array?

Lets say I have the following map:
let myMap = new Map().set('a', 1).set('b', 2);
And I want to obtain ['a', 'b'] based on the above. My current solution seems so long and horrible.
let myMap = new Map().set('a', 1).set('b', 2);
let keys = [];
for (let key of myMap)
keys.push(key);
console.log(keys);
There must be a better way, no?
Map.keys() returns a MapIterator object which can be converted to Array using Array.from:
let keys = Array.from( myMap.keys() );
// ["a", "b"]
EDIT: you can also convert iterable object to array using spread syntax
let keys =[ ...myMap.keys() ];
// ["a", "b"]
You can use the spread operator to convert Map.keys() iterator in an Array.
let myMap = new Map().set('a', 1).set('b', 2).set(983, true)
let keys = [...myMap.keys()]
console.log(keys)
OK, let's go a bit more comprehensive and start with what's Map for those who don't know this feature in JavaScript... MDN says:
The Map object holds key-value pairs and remembers the original
insertion order of the keys.
Any value (both objects and primitive
values) may be used as either a key or a value.
As you mentioned, you can easily create an instance of Map using new keyword...
In your case:
let myMap = new Map().set('a', 1).set('b', 2);
So let's see...
The way you mentioned is an OK way to do it, but yes, there are more concise ways to do that...
Map has many methods which you can use, like set() which you already used to assign the key values...
One of them is keys() which returns all the keys...
In your case, it will return:
MapIterator {"a", "b"}
and you easily convert them to an Array using ES6 ways, like spread operator...
const b = [...myMap.keys()];
I need something similiar with angular reactive form:
let myMap = new Map().set(0, {status: 'VALID'}).set(1, {status: 'INVALID'});
let mapToArray = Array.from(myMap.values());
let isValid = mapToArray.every(x => x.status === 'VALID');
Not exactly best answer to question but this trick new Array(...someMap) saved me couple of times when I need both key and value to generate needed array. For example when there is need to create react components from Map object based on both key and value values.
let map = new Map();
map.set("1", 1);
map.set("2", 2);
console.log(new Array(...map).map(pairs => pairs[0])); -> ["1", "2"]
Side note, if you are using a JavaScript object instead of a map, you can use Object.keys(object) which will return an array of the keys. Docs: link
Note that a JS object is different from a map and can't necessarily be used interchangeably!

JSON stringify a Set

How would one JSON.stringify() a Set?
Things that did not work in Chromium 43:
var s = new Set(['foo', 'bar']);
JSON.stringify(s); // -> "{}"
JSON.stringify(s.values()); // -> "{}"
JSON.stringify(s.keys()); // -> "{}"
I would expect to get something similar to that of a serialized array.
JSON.stringify(["foo", "bar"]); // -> "["foo","bar"]"
JSON.stringify doesn't directly work with sets because the data stored in the set is not stored as properties.
But you can convert the set to an array. Then you will be able to stringify it properly.
Any of the following will do the trick:
JSON.stringify([...s]);
JSON.stringify([...s.keys()]);
JSON.stringify([...s.values()]);
JSON.stringify(Array.from(s));
JSON.stringify(Array.from(s.keys()));
JSON.stringify(Array.from(s.values()));
You can pass a "replacer" function to JSON.stringify:
const fooBar = {
foo: new Set([1, 2, 3]),
bar: new Set([4, 5, 6])
};
JSON.stringify(
fooBar,
(_key, value) => (value instanceof Set ? [...value] : value)
);
Result:
"{"foo":[1,2,3],"bar":[4,5,6]}"
toJSON is a legacy artifact, and a better approach is to use a custom replacer, see https://github.com/DavidBruant/Map-Set.prototype.toJSON/issues/16
While all of the above work I suggest that you subclass set and add a toJSON method to make sure that it stringify's correctly. Especially if you are going to be stringifying often. I use sets in my Redux stores and needed to make sure this was never a problem.
This is a basic implementation. Naming is just to illustrate the point pick your own style.
class JSONSet extends Set {
toJSON () {
return [...this]
}
}
const set = new JSONSet([1, 2, 3])
console.log(JSON.stringify(set))
The problem with all the previous approaches is that they all convert the set into Array, which is missing the entire point of Set and indexes.
What you should do is to use an Object instead.
Either convert it with the following function or simply create it as Object instead of Set.
const mySet = new Set(['hello', 'world']);
const myObj = {};
for (let value of mySet.values()) {
myObj[value] = true;
}
Then instead of using mySet.has('hello')
Do myObj.hasOwnProperty('hello').
Then stringify it as an object without a problem.
Note:
The following method uses more memory because it needs to store the value as well as the key.
But performence wise it's still O(1) compared to Array.includes() which is O(n) and miss the point of even using a Set.

Categories

Resources