ES6 introduces two welcome data structures: maps and sets. Maps are similar to objects in that they can map a key to a value, and sets are similar to arrays except that duplicates are not allowed.
Prior to ES6, when you needed to map keys to values, you would turn to an object, because objects allow you to map string keys to object values of any type. However, using objects for this purpose has many drawbacks:
The prototypal nature of objects means that there could be mappings that you didn’t intend.
There’s no easy way to know how many mappings there are in an object.
Keys must be strings or symbols, preventing you from mapping objects to values.
Objects do not guarantee any order to their properties.
The Map
object addresses these deficiencies, and is a superior choice for mapping keys to values (even if the keys are strings). For example, imagine you have user objects you wish to map to roles:
const
u1
=
{
name
:
'Cynthia'
};
const
u2
=
{
name
:
'Jackson'
};
const
u3
=
{
name
:
'Olive'
};
const
u4
=
{
name
:
'James'
};
You would start by creating a map:
const
userRoles
=
new
Map
();
Then you can use the map to assign users to roles by using its set()
method:
userRoles
.
set
(
u1
,
'User'
);
userRoles
.
set
(
u2
,
'User'
);
userRoles
.
set
(
u3
,
'Admin'
);
// poor James...we don't assign him a role
The set()
method is also chainable, which can save some typing:
userRoles
.
set
(
u1
,
'User'
)
.
set
(
u2
,
'User'
)
.
set
(
u3
,
'Admin'
);
You can also pass an array of arrays to the constructor:
const
userRoles
=
new
Map
([
[
u1
,
'User'
],
[
u2
,
'User'
],
[
u3
,
'Admin'
],
]);
Now if we want to determine what role u2
has, you can use the get()
method:
userRoles
.
get
(
u2
);
// "User"
If you call get
on a key that isn’t in the map, it will return undefined
. Also, you can use the has()
method to determine if a map contains a given key:
userRoles
.
has
(
u1
);
// true
userRoles
.
get
(
u1
);
// "User"
userRoles
.
has
(
u4
);
// false
userRoles
.
get
(
u4
);
// undefined
If you call set()
on a key that’s already in the map, its value will be replaced:
userRoles
.
get
(
u1
);
// 'User'
userRoles
.
set
(
u1
,
'Admin'
);
userRoles
.
get
(
u1
);
// 'Admin'
The size
property will return the number of entries in the map:
userRoles
.
size
;
// 3
Use the keys()
method to get the keys in a map, values()
to return the values, and entries()
to get the entries as arrays where the first element is the key and the second is the value. All of these methods return an iterable object, which can be iterated over by a for...of
loop:
for
(
let
u
of
userRoles
.
keys
())
console
.
log
(
u
.
name
);
for
(
let
r
of
userRoles
.
values
())
console
.
log
(
r
);
for
(
let
ur
of
userRoles
.
entries
())
console.log(
`
${
ur
[
0
].
name
}
:
${
ur
[
1
]
}
`
);
// note that we can use destructuring to make
// this iteration even more natural:
for
(
let
[
u
,
r
]
of
userRoles
.
entries
())
console.log(
`
${
u
.
name
}
:
${
r
}
`
);
// the entries() method is the default iterator for
// a Map, so you can shorten the previous example to:
for
(
let
[
u
,
r
]
of
userRoles
)
console.log(
`
${
u
.
name
}
:
${
r
}
`
);
If you need an array (instead of an iterable object), you can use the spread operator:
[
...
userRoles
.
values
()];
// [ "User", "User", "Admin" ]
To delete a single entry from a map, use the delete()
method:
userRoles
.
delete
(
u2
);
userRoles
.
size
;
// 2
Lastly, if you want to remove all entries from a map, you can do so with the clear()
method:
userRoles
.
clear
();
userRoles
.
size
;
// 0
A WeakMap
is identical to Map
except:
Its keys must be objects.
Keys in a WeakMap
can be garbage-collected.
A WeakMap
cannot be iterated or cleared.
Normally, JavaScript will keep an object in memory as long as there is a reference to it somewhere. For example, if you have an object that is a key in a Map
, JavaScript will keep that object in memory as long as the Map
is in existence. Not so with a WeakMap
. Because of this, a WeakMap
can’t be iterated (there’s too much danger of the iteration exposing an object that’s in the process of being garbage-collected).
Thanks to these properties of WeakMap
, it can be used to store private keys in object instances:
const
SecretHolder
=
(
function
()
{
const
secrets
=
new
WeakMap
();
return
class
{
setSecret
(
secret
)
{
secrets
.
set
(
this
,
secret
);
}
getSecret
()
{
return
secrets
.
get
(
this
);
}
}
})();
Here we’ve put our WeakMap
inside an IIFE, along with a class that uses it. Outside the IIFE, we get a class that we call SecretHolder
whose instances can store secrets. We can only set a secret through the setSecret
method, and only get the secret through the getSecret
method:
const
a
=
new
SecretHolder
();
const
b
=
new
SecretHolder
();
a
.
setSecret
(
'secret A'
);
b
.
setSecret
(
'secret B'
);
a
.
getSecret
();
// "secret A"
b
.
getSecret
();
// "secret B"
We could have used a regular Map
, but then the secrets we tell instances of SecretHolder
could never be garbage-collected!
A set is a collection of data where duplicates are not allowed (consistent with sets in mathematics). Using our previous example, we may want to be able to assign a user to multiple roles. For example, all users might have the "User"
role, but administrators have both the "User"
and "Admin"
role. However, it makes no logical sense for a user to have the same role multiple times. A set is the ideal data structure for this case.
First, create a Set
instance:
const
roles
=
new
Set
();
Now if we want to add a user role, we can do so with the add()
method:
roles
.
add
(
"User"
);
// Set [ "User" ]
To make this user an administrator, call add()
again:
roles
.
add
(
"Admin"
);
// Set [ "User", "Admin" ]
Like Map
, Set
has a size
property:
roles
.
size
;
// 2
Here’s the beauty of sets: we don’t have to check to see if something is already in the set before we add it. If we add something that’s already in the set, nothing happens:
roles
.
add
(
"User"
);
// Set [ "User", "Admin" ]
roles
.
size
;
// 2
To remove a role, we simply call delete()
, which returns true
if the role was in the set and false
otherwise:
roles
.
delete
(
"Admin"
);
// true
roles
;
// Set [ "User" ]
roles
.
delete
(
"Admin"
);
// false
Weak sets can only contain objects, and the objects they contain may be garbage-collected. As with WeakMap
, the values in a WeakSet
can’t be iterated, making weak sets very rare; there aren’t many use cases for them. As a matter of fact, the only use for weak sets is determining whether or not a given object is in a set or not.
For example, Santa Claus might have a WeakSet
called naughty
so he can determine who to deliver the coal to:
const
naughty
=
new
WeakSet
();
const
children
=
[
{
name
:
"Suzy"
},
{
name
:
"Derek"
},
];
naughty
.
add
(
children
[
1
]);
for
(
let
child
of
children
)
{
if
(
naughty
.
has
(
child
))
console.log(
`Coal for
${
child
.
name
}
!`
);
else
console.log(
`Presents for
${
child
.
name
}
!`
);
}
If you’re an experienced JavaScript programmer who’s new to ES6, chances are objects are your go-to choice for mapping. And no doubt you’ve learned all of the tricks to avoiding the pitfalls of objects as maps. But now you have real maps, and you should use them! Likewise, you’re probably accustomed to using objects with boolean values as sets, and you don’t have to do that anymore, either. When you find yourself creating an object, stop and ask yourself, “Am I using this object only to create a map?” If the answer is “Yes,” consider using a Map
instead.
3.17.79.60