Traversing an object in an elegant, reliable, safe, and fast way
It is a common case when we have a key-value object (let's say options) and need to iterate it. There is an academic way to do this, as shown in the following code:
"use strict"; var options = { bar: "bar", foo: "foo" }, key; for( key in options ) { console.log( key, options[ key] ); }
The preceding code outputs the following:
bar bar foo foo
Now let's imagine that any of the third-party libraries that you load in the document augments the built-in Object
:
Object.prototype.baz = "baz";
Now when we run our example code, we will get an extra undesired entry:
bar bar foo foo baz baz
The solution to this problem is well known, we have to test the keys with the Object.prototype.hasOwnProperty
method:
//… for( key in options ) { if ( options.hasOwnProperty( key ) ) { console.log( key, options[ key] ); } }
Iterating the key-value object safely and fast
Let's face the truth—the structure is clumsy and requires optimization (we have to perform the hasOwnProperty
test on every given key). Luckily, JavaScript has the Object.keys
method that retrieves all string-valued keys of all enumerable own (non-inherited) properties. This gives us the desired keys as an array that we can iterate, for instance, with Array.prototype.forEach
:
"use strict"; var options = { bar: "bar", foo: "foo" }; Object.keys( options ).forEach(function( key ){ console.log( key, options[ key] ); });
Besides the elegance, we get a better performance this way. In order to see how much we gain, you can run this online test in distinct browsers such as: http://codepen.io/dsheiko/pen/JdrqXa.
Enumerating an array-like object
Objects such as arguments
and nodeList
(node.querySelectorAll
, document.forms
) look like arrays, in fact they are not. Similar to arrays, they have the length
property and can be iterated in the for
loop. In the form of objects, they can be traversed in the same way that we previously examined. But they do not have any of the array manipulation methods (forEach
, map
, filter
, some
and so on). The thing is we can easily convert them into arrays as shown here:
"use strict"; var nodes = document.querySelectorAll( "div" ), arr = Array.prototype.slice.call( nodes ); arr.forEach(function(i){ console.log(i); });
The preceding code can be even shorter:
arr = [].slice.call( nodes )
It's a pretty convenient solution, but looks like a trick. In ES6, we can do the same conversion with a dedicated method:
arr = Array.from( nodes );
The collections of ES6
ES6 introduces a new type of objects—iterable objects. These are the objects whose elements can be retrieved one at a time. They are quite the same as iterators in other languages. Beside arrays, JavaScript received two new iterable data structures, Set
and Map
. Set
which are a collection of unique values:
"use strict"; let foo = new Set(); foo.add( 1 ); foo.add( 1 ); foo.add( 2 ); console.log( Array.from( foo ) ); // [ 1, 2 ] let foo = new Set(), bar = function(){ return "bar"; }; foo.add( bar ); console.log( foo.has( bar ) ); // true
The map is similar to a key-value object, but may have arbitrary values for the keys. And this makes a difference. Imagine that we need to write an element wrapper that provides jQuery-like events API. By using the on
method, we can pass not only a handler callback function but also a context (this
). We bind the given callback to the cb.bind( context )
context. This means addEventListener
receives a function reference different from the callback. How do we unsubscribe the handler then? We can store the new reference in Map
by a key composed from an event name and a callback
function reference:
"use strict"; /** * @class * @param {Node} el */ let El = function( el ){ this.el = el; this.map = new Map(); }; /** * Subscribe a handler on event * @param {String} event * @param {Function} cb * @param {Object} context */ El.prototype.on = function( event, cb, context ){ let handler = cb.bind( context || this ); this.map.set( [ event, cb ], handler ); this.el.addEventListener( event, handler, false ); }; /** * Unsubscribe a handler on event * @param {String} event * @param {Function} cb */ El.prototype.off = function( event, cb ){ let handler = cb.bind( context ), key = [ event, handler ]; if ( this.map.has( key ) ) { this.el.removeEventListener( event, this.map.get( key ) ); this.map.delete( key ); } };
Any iterable object has methods, keys
, values
, and entries
, where the keys work the same as Object.keys
and the others return array values and an array of key-value pairs respectively. Now let's see how we can traverse the iterable objects:
"use strict"; let map = new Map() .set( "bar", "bar" ) .set( "foo", "foo" ), pair; for ( pair of map ) { console.log( pair ); } // OR let map = new Map([ [ "bar", "bar" ], [ "foo", "foo" ], ]); map.forEach(function( value, key ){ console.log( key, value ); });
Iterable objects have manipulation methods such as arrays. So we can use forEach
. Besides, they can be iterated by for...in
and for...of
loops. The first one retrieves indexes and the second, the values.