📜 ⬆️ ⬇️

Real Associative Arrays in JavaScript

The use of an object literal as a simple means of storing key-value pairs has long been commonplace in JavaScript. However, the object literal is not a true associative array, and therefore, in some situations, its use can lead to unexpected results. While JS does not provide a native implementation of associative arrays (not in all browsers, at least), there is an excellent alternative to objects with the necessary functionality and without pitfalls.

Problem with objects


The problem lies in the chain of prototypes. Any new object inherits properties and methods from Object.prototype, which can prevent us from uniquely determining the existence of a key. Take for example the toString method, checking for the presence of a key with the same name using the in operator will lead to a false positive result:

 var map = {}; 'toString' in map; // true 

This happens because the in operator, not finding a property in an object instance, looks further along the chain of prototypes in search of inherited values. In our case, this is the toString method. To solve this problem, there is a method hasOwnProperty , which was designed specifically to check the presence of properties only in the current object:

 var map = {}; map.hasOwnProperty('toString'); // false 

This technique works fine as long as you don’t hit the key named hasOwnProperty. Overwriting this method will result in its subsequent calls leading to unpredictable results or errors, depending on the new value:
')
 var map = {}; map.hasOwnProperty = 'foo'; map.hasOwnProperty('hasOwnProperty'); // TypeError 

We quickly fix this problem. To do this, use another, intact object and call its hasOwnProperty method in the context of our object:

 var map = {}; map.hasOwnProperty = 'foo'; {}.hasOwnProperty.call(map, 'hasOwnproperty'); // true 

Here, this method already works without problems, but still it imposes some restrictions on how we will use it. For example, every time you want to list the properties of your object with for ... in , you have to filter out all the inherited stuff:

 var map = {}; var has = {}.hasOwnProperty; for(var key in map){ if(has.call(map, key)){ // do something } } 

After some time, this method will tire you terribly. Thank God there is a better option.

Bare objects


The secret to creating a pure associative array in getting rid of the prototype and all the baggage that he carries with him. To accomplish this, we use the Object.create method provided in ES5 . The uniqueness of this method is that you can clearly define the prototype of a new object. For example, create a normal object a little more clearly:

 var obj = {}; //   : var obj = Object.create(Object.prototype); 

In addition to the fact that you can choose any prototype, the method also gives you the opportunity not to select the prototype at all, simply by passing null instead:

 var map = Object.create(null); map instanceof Object; // false Object.prototype.isPrototypeOf(map); // false Object.getPrototypeOf(map); // null 

These bare objects (or dictionaries) are ideal for creating associative arrays, since the absence of [[Prototype]] removes the risk of stumbling upon name conflicts. And even better! After we have deprived the object of all inherited methods and properties, any attempts to use it for purposes other than its intended purpose (repository) will result in errors:

 var map = Object.create(null); map + ""; // TypeError: Cannot convert object to primitive value 

There is no primitive value or string representation. Bare objects are only intended to work as a repository of key-value pairs and a period.

Keep in mind that the hasOwnProperty method hasOwnProperty also no longer there, and it is not needed, since the in operator now works fine without any checks.

 var map = Object.create(null); 'toString' in map; // false 

Moreover, those tedious for ... in cycles are now much easier. Finally, we can safely write them the way they should look like:

 var map = Object.create(null); for(var key in map){ // do something } 

Despite the changes, we can still do everything we need with objects, such as using dot notation or square brackets, turning them into a string, or using an object as a context for any method from Object.prototype :

 var map = Object.create(null); Object.defineProperties(map, { 'foo': { value: 1, enumerable: true }, 'bar': { value: 2, enumerable: false } }); map.foo; // 1 map['bar']; // 2 JSON.stringify(map); // {"foo":1} {}.hasOwnProperty.call(map, 'foo'); // true {}.propertyIsEnumerable.call(map, 'bar'); // false 

Even the various types of type checking will still work:

 var map = Object.create(null); typeof map; // object {}.toString.call(map); // [object Object] {}.valueOf.call(map); // Object {} 

All this makes it possible to easily replace the usual objects used to create associative arrays with bare objects, both in new and in any previously created applications.

Conclusion


If we talk about simple storages of key-value pairs, then bare objects will cope with this task unambiguously better than ordinary objects, saving the developer from all that is superfluous. For more functional data structures, we will have to wait for ES6 (ES2015), which will provide us with native associative arrays in the form of Map , Set, and other objects. In the meantime, this rainbow moment has not arrived, bare objects are the best choice.

Source: https://habr.com/ru/post/259529/


All Articles