I was asked during an interview to implement a data structure of key/value pairs where keys can be an object, I know this is possible using ES6 maps but how do they work under the hood in Javascript where keys are strictly stringified and yet achieve the same constant lookup time of a hash table / object?
Thank you.
The keys of Map are not keys of the Map object. They are arguments to the .get and .set methods.
const map = new Map;
const key = {};
map.set(key, "stuff"); // key is passed as an argument, not stringified
map[key] // key would get stringified here according to language semantics, but even then it would be undefined as maps keys aren't keys of the map object
What these methods do under the hood is implementation secific.
I was asked this on an interview, and I can't vouch for how Map is actually implemented under the hood (ended up here looking for myself). However, this is the approach I worked out with my interviewer (note, the requirement was only for DOM node keys, but I do think objects are the trickiest key to handle, and the rest could be handled trivially with additional code), and I think it is at least insightful:
class Map {
constructor() {
this.map = {}; // internal key/value object
this.trackerKey = Symbol();
}
set(key, value) {
let lookupKey = key[this.trackerKey];
if (!lookupKey) {
lookupKey = Symbol();
key[this.trackerKey] = lookupKey;
}
this.map[lookupKey] = value;
}
has(key) {
return key.hasOwnProperty(this.trackerKey);
}
get(key) {
const lookupKey = key[this.trackerKey];
return this.map[lookupKey];
}
delete(key) {
const lookupKey = key[this.trackerKey];
delete key[this.trackerKey];
delete this.map[lookupKey];
}
}
Basically, the idea is to use Symbols under the hood to keep track of the objects. trackerKey is a (Symbol) property we add to all keys that are coming in. Since it's a symbol defined inside the instance, nothing else will be able to reference it.
So when we go to set, we check if the trackerKey property exists on the object. If not, we set it to a new symbol. We then set the value of that key in our internal map to the value coming in.
has and get are fairly straightforward lookups now. We check if our tracker key exists on the key to see if it is included in our Map. For get we can simply grab our internal lookup key for the object from its trackerKey property.
For delete, we just need to delete the trackerKey property from the key object, and then delete the property in our internal map object.
Fun and insightful exercise! Hope this helps :)
Related
I am reading this Angular doc, it says:
With any object-like expression—such as object, Array, Map, or Set—the identity of the object must change for Angular to update the class list. Updating the property without changing object identity has no effect.
So, how do I change an object't identity?
Thanks.
The wording there is slightly unfortunate. You can't really change an object's identity - the identity is that object. It's like trying to make X not X. What it means is that a new object or array needs to be created for Angular to detect it as different. For a tiny example in vanilla JS:
const isSameObject = (obj1, obj2) => obj1 === obj2;
console.log(isSameObject({}, {})); // false, different objects
const someObj = {};
const sameReferenceToSomeObj = someObj;
sameReferenceToSomeObj.newProp = 'newVal';
console.log(isSameObject(someObj, sameReferenceToSomeObj)); // true, same object
For Angular to detect a change the identity on a property, the property value must be changed to a new value, rather than having the old value mutated. The second example in the snippet above is an example of how not to do things in Angular; mutating an object doesn't change its identity (it's still the same object), so if you did that in Angular, it would see that the old object on the property is === to the new object on the property, and would not produce a visual change as a result.
Create an entirely new object instead, with the desired properties, and put that object on the component, and then Angular will be able to see that the new object is not === to the old object, and take the appropriate action as a result, eg
this.theClassList = { ...this.theClassList, newProperty: 'foo' }
and not
const newClassList = this.theClassList;
newClassList.newProperty = 'foo';
this.theClassList = newClassList;
I have this small function (within my Angular 7 application) which uses JavaScript reduce(), and locates an object within a nested array of objects. I can then proceed to update certain properties on the fly.
Now, in addition to this find logic, I would like to also insert/delete an object into/from the nested array.
Question is: once I do locate my object, can I push() and/or delete an object ?
const input={UID:2,GUID:"",LocationName:"USA",ParentLocation:null,subs:[{UID:42,GUID:"",LocationName:"New Jersey",Description:"",subs:[{UID:3,GUID:"",LocationName:"Essex County",ParentLocation:null,"subs":[{UID:4,LocationName:"Newark",ParentLocation:3,"subs":[{"UID":49,"GUID":"","LocationName":"Doctor Smith's Office","LocationType":{"UID":2,"LocationTypeName":"Practice","Description":"other location"},"subs":[{"HostID":38,"HostName":"Ocean Host",}]}]}]}]}]};
const findUIDObj = (uid, parent) => {
const { UID, subs } = parent;
if (UID === uid) {
const { subs, ...rest } = parent;
return rest;
}
if (subs) return subs.reduce((found, child) => found || findUIDObj(uid, child), null);
};
console.log(findUIDObj(49, input));
var obj = findUIDObj(49, input);
delete obj;
For example, in my Angular 7 app, it complains if I attempt to delete the found object:
ex/
var obj = findUIDObj(49, input);
delete obj;
'delete' cannot be called on an identifier in strict mode.
Looking briefly at your code, I see you are using a const identifier to declare your data collection. We only use const for static data that does not change, and that’s the purpose of it. So, first of all matters, that seems to be the problem. To test it change it to let. Now, as for methods for data management, immutability is worthy of your consideration for many reasons, but namely Angular will rerender the entire object regardless of altering the existing object, or receiveing a new object. You can look up Immutable JavaScript to understand more. In many cases, creating immutable data management is done with a library, you can do it yourself. Basically, create a function called copy( data ), or something so that you pass in the original object, but you get a copy of it in return with no reference to the original object. That way one does not accidentally change the original object. To do this you can do this inside of your copy function: return JSON.parse(JSON.stringify( data )) ;
The only problem you might run into here is deep nested objects, or objects with circular references can cause problems. I have an overriding stringify method to mange this in little libraries I’ve written.
delete obj would never do what you want: first of all, it is not even an object from your input, since the function created a new object from the found object, excluding the subs property, and returned that. But more importantly, delete is used for deleting properties, not objects.
It seems you want to remove a matching object from its parent subs property. For that you would need to mutate the subs array, so it would exclude the matching object. For that to work in a generic way, your input should be an array. Otherwise that root object could not be removed from anything.
With that in mind, your lookup function should return the array in which the match was found and at which index. With those pieces of information you can decide to remove that element from the array, or to insert another object at that index.
Here is how it could work with removal:
const input=[{UID:2,GUID:"",LocationName:"USA",ParentLocation:null,subs:[{UID:42,GUID:"",LocationName:"New Jersey",Description:"",subs:[{UID:3,GUID:"",LocationName:"Essex County",ParentLocation:null,"subs":[{UID:4,LocationName:"Newark",ParentLocation:3,"subs":[{"UID":49,"GUID":"","LocationName":"Doctor Smith's Office","LocationType":{"UID":2,"LocationTypeName":"Practice","Description":"other location"},"subs":[{"HostID":38,"HostName":"Ocean Host",}]}]}]}]}]}];
const findUIDObj = (uid, arr) => {
if (!arr) return;
const idx = arr.findIndex(obj => obj.UID === uid);
if (idx > -1) return [arr, idx];
for (const obj of arr) {
const result = findUIDObj(uid, obj.subs);
if (result) return result;
}
};
console.log(findUIDObj(49, input));
const [arr, idx] = findUIDObj(49, input) || [];
if (arr) {
arr.splice(idx, 1); // Remove object from its parent array
}
According to MDN, the size property of a Map object should represent the number of entries the Map object has. However, if I create an empty Map and use the bracket notation (not sure what the formal name for this is) to add a key/value pair, the size property is not updated. However, if I use the Map.set(key, value) function, then the property is updated. Why is this?
Example:
var ex = new Map();
ex['key1'] = 'value1';
ex.set('key2', 'value2');
After running this, ex.size returns 1.
Because adding a property to the map object is not the same as adding an element to the map (and vice versa). Try ex.get('key1') (and ex['key2']). Only Map#set adds an element to the map. Maybe it becomes clearer if you consider this poor and limited implementation of Map:
class Map {
constructor() {
this._items = {};
}
set(key, value) {
this._items[key] = value;
}
get(key) {
return this._items[key];
}
get size() {
return Object.keys(this._items).length;
}
}
Adding a property to an instance of the class would not affect this._items, which is where the map entries are stored.
You can add arbitrary properties to basically any object in JavaScript, but that doesn't mean that the object is doing anything with them.
The most confusing case is probably arrays. Only properties whose numeric value is between 0 and 2^32 are considered "elements" of the array. So when I do
var arr = [];
arr.foo = 42;
then arr.length will still be 0.
Because ex['key1'] = 'value1'; just added a new property key1 to the object ex (that happens to be of type Map).
It does not affect the content of the actual Map.
I am using Map
because I want to store an object as a key.
My question is - can I access a map the same way I would access a plain object?
For example:
let m = new Map();
let obj = {foo:'bar'};
m[obj] = 'baz';
console.log(m[obj]);
is this supposed to work correctly as is, or do I need to use the get/set methods of a Map?
The reason I ask is because if I need to use get/set it forces to me to carefully refactor a lot of code.
Here is a real life example of code that may need to be refactored:
// before (broker.wsLock was plain object)
function addWsLockKey(broker, ws, key) {
let v;
if (!( v = broker.wsLock[ws])) {
v = broker.wsLock[ws] = [];
}
if (v.indexOf(key) < 0) {
v.push(key);
}
}
// after (broker.wsLock is a Map instance)
function addWsLockKey(broker, ws, key) {
let v;
if (!( v = broker.wsLock.get(ws))) {
v = [];
broker.wsLock.set(ws, v);
}
if (v.indexOf(key) < 0) {
v.push(key);
}
}
is there some way to set v on the same line as the set() call?
If you want access to the actual values of the Map object, then you have to use .get() and .set() or various iterators.
var m = new Map();
m.set("test", "foo");
console.log(m.get("test")); // "foo"
Regular property access on a Map such as:
m["test"] = "foo"
just sets a regular property on the object - it does not affect the actual map data structure.
I imagine it was done this way so that you can access the Map object properties separately from the members of the Map data structure and the two shall not be confused with one another.
In addition, regular properties will only accept a string as the property name so you can't use a regular property to index an object. But, map objects have that capability when using .set() and .get().
You asked for a "definitive" answer to this. In the ES6 specification, you can look at what .set() does and see that it operates on the [[MapData]] internal slot which is certainly different than the properties of an object. And, likewise, there is no where in that spec where it says that using normal property access would access the internal object [[MapData]]. So, you'll have to just see that normal property access is describe for an Object. A Map is an Object and there's nothing in the Map specification that says that normal property access should act any different than it does for any other object. In fact, it has to act the same for all the methods on the Map object or they wouldn't work if you happened to put an item in the Map with the same key as a method name. So, you're proof consists of this:
A simple test will show you that property access does not put anything in the Map itself, only a regular property.
The spec describes a Map as an object.
The spec describes how .get() and .set() operate on the internal slot [[MapData]].
There's nothing in the spec that says property access on a Map object should work any different than it always does.
If property access did access the MapData, then you would not be able to access methods if you happened to put a key in the Map that conflicted with a method name - that would be a mess if that was the case.
After reading this description: http://wiki.ecmascript.org/doku.php?id=harmony:weak_maps
I'm trying to get a hang of it, but I do not get the overall picture. What is it all about? It seems to be supported in Firefox 6: http://kangax.github.com/es5-compat-table/non-standard/
A weak reference is a special object containing an object-pointer, but does not keep that object alive.
One application of weak references are implemented in Weak Maps:
“The experienced JavaScript programmer will notice that this API could be implemented in JavaScript with two arrays (one for keys, one for values) shared by the 4 API methods. Such an implementation would have two main inconveniences. The first one is an O(n) search (n being the number of keys in the map). The second one is a memory leak issue. With manually written maps, the array of keys would keep references to key objects, preventing them from being garbage collected. In native WeakMaps, references to key objects are held “weakly”, which means that they do not prevent garbage collection in case there would be no other reference to the object.” Source
(See also my post when ECMAScript Harmony was first released with Firefox... )
WeakMap
WeakMaps basically allow you to have a HashTable with a key that isn't a String.
So you can set the key to be, i.e. [1] and then can say Map.get([1])
Example from the MDN:
var wm1 = new WeakMap(),
wm2 = new WeakMap();
var o1 = {},
o2 = function(){},
o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because there is no value for o2 on wm2
wm2.get(o3); // undefined, because that is the set value
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (even if the value itself is 'undefined')
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
The reason for its existance is:
in order to fix a memory leak present in many uses of weak-key tables.
Apparently emulating weakmaps causes memory leaks. I don't know the details of those memory leaks.
WeakMap allows to use objects as keys.
It does not have any method to know the length of the map. The length is always 1.
The key can't be primitive values
A word of caution about using object as key is, since all the objects are by default singletons in JavaScript we should be creating an object reference and use it.
This is because when we create anonymous objects they are different.
if ( {} !== {} ) { console.log('Objects are singletons') };
// will print "Objects are singletons"
So in the following scenario, we can't expect to get the value
var wm = new WeakMap()
wm.set([1],'testVal');
wm.get([1]); // will be undefined
And the following snippet will work as expected.
var a = [1];
wm.set(a, 'testVal');
wm.get(a); // will return 'testVal'