Iterating through JavaScript objects and arrays

In this article we’ll have a look at how to iterate through javascript objects and arrays with Mangler.js. I’ll also show you how to make your own objects iterable, and how it enhances other powerful features of the library.

Iterating through object literals and arrays

By default Mangler.js recognises 3 types of objects as iterable: object literals, arrays and mangler objects. To iterate through these objects, simply call Mangler.each() and pass a callback function in the form of callback(key, value):

var order = [
	{ product: 1, qty: 2, price: 1.00 },
	{ product: 2, qty: 1, price: 1.50 },
	{ product: 3, qty: 3, price: 2.00 }
];

var total = 0;
Mangler.each(order, function(index, item) {
	total += item.price * item.qty;
});

The callback function will be called for every key/value pair:

  • Arrays: The callback is called for every item in the array, passing the numeric item index as key and the item itself as the value.
  • Objects: The callback is called for every object property, passing the property name for the key as string and the property value.

If the callback function returns false, iteration will stop.

Recursive iteration

With Mangler.explore(), you can recursively iterate through all iterable children as well. In addition to the key and the value, the callback function gets a path and a state parameter too:

  • The path is a string containing the absolute path of the current item’s parent from the root of the iteration, e.g. root.array[0].parent.
  • The state is an optional object which is passed down the hierarchy. It is shallow copied at every level, any changes made to it are only visible to the current item’s siblings and children.

Here is a simple example which adds a depth property to all nested objects:

var data = [
	{
		child: {}
	},
	{
		children: [{}, {}]
	}
];

// Optional prefix to the callback's path parameter
var rootPath = "";

// State object passed at root level
var initialState = { depth: 0 };

Mangler.explore(data, function(key, value, path, state) {
	if(Mangler.isObject(value)) {
		if(path !== state.path) {
			// There was a change in parent path, increase depth
			// It makes sure we don't increase depth for every sibling
			state.path = path;
			state.depth++;
		}
		value.depth = state.depth;
	}
}, rootPath, initialState);

Resulting data array:

var data = [
	{
		depth: 1,
		child: { depth: 2 }
	},
	{
		depth: 1,
		children: [{ depth: 2 }, { depth: 2 }]
	}
];

In case of Mangler.explore(), if the callback function returns false, iteration will continue with the current item’s next sibling, skipping all of its children.

Iterating through other object instances

You can set up your own objects to be iterable by Mangler.js by registering it with Mangler.registerType():

// Simple custom object
function Company() {
	this.name = 'Example Co.';
	this.director = 'John Smith';
	this.employees = ['Fred Bloggs', 'Someone Else'];
}

// Make all people iterable like an array
Mangler.registerType(Company, {
	each: function(obj, callback) {
		var i,index = 0;
		
		if(callback(index++, obj.director) === false) return;
		for(i = 0; i < obj.employees.length; i++) {
			if(callback(index++, obj.employees[i]) === false) return;
		}
	}
});

// Iterate through items
Mangler.each(new Company(), function(key, value) {
	console.log(key, value);
});

The registered each function must conform to the above mentioned standard: iteration has to stop if the callback function returns false.

There are a couple of standard implementations where you don’t need to register an each function directly, provided that your object supports it:

The first standard method can be used if your object already has a standard each method. In this case, simply set the each option to true. Here’s the same example again with standard iterator method:

// Simple custom object
function Company() {
	this.name = 'Example Co.';
	this.director = 'John Smith';
	this.employees = ['Fred Bloggs', 'Someone Else'];
}

// Standard iterator method
Company.prototype.each = function(callback) {
	var i,index = 0;
	
	if(callback(index++, this.director) === false) return;
	for(i = 0; i < this.employees.length; i++) {
		if(callback(index++, this.employees[i]) === false) return;
	}
};

// Make all people iterable like an array
Mangler.registerType(Company, { each: true });

The second standard method is built-in support for array-like objects, e.g. the built-in typed arrays. If your object has a length property and its items can be accessed by indices, set the each option to 'array':

// The simplest array-like container
function Blackhole() {
	this.length = 0;
}

Blackhole.prototype.add = function(item) {
	this[this.length++] = item;
	return this;
};

// Register array-like iterator
Mangler.registerType(Blackhole, { each: 'array' });

// Create instance
var b = new Blackhole();
b.add(10).add(20).add(30);

// Iterate
Mangler.each(b, function(key, value) {
	// Do stuff...
});

Advantages of making your objects iterable

Which objects you make iterable is entirely up to you depending on what you want to achieve. The Mangler.each() and Mangler.explore() functions are used extensively by the library itself. By making your object iterable, you enable Mangler.js to “look into” your objects.

Once you register an each function, Mangler.explore() will be able to recurse into your object and process its children. Depending on the type of key you call the callback function with, the explore path will be built differently:

  • For numeric keys, path will be built like an array item reference: parent[0]
  • For string keys, path will be build like an object property reference: parent.child

To go further, Mangler.extract() uses .explore() to extract parts of your data. By making your object iterable, you also allow .extract() to look into your object and extract parts of it using a simple path filter:

function Blackhole() {
	this.length = 0;
}

Blackhole.prototype.add = function(item) {
	this[this.length++] = item;
	return this;
};

// Wrap our new object inside an object literal
var data = {
	name: 'Rob',
	blackhole: (new Blackhole()).add({ name: 'John' }).add({ name: 'Fred' })
};

// Extract name properties before registering
var before = Mangler.extract(data, 'name');
// before = ['Rob']

// Register array-like iterator
Mangler.registerType(Blackhole, { each: 'array' });

// Same extraction after registration
var after = Mangler.extract(data, 'name');
// after = ['Rob', 'John', 'Fred']
Share Button
Tagged with: ,
0 comments on “Iterating through JavaScript objects and arrays
1 Pings/Trackbacks for "Iterating through JavaScript objects and arrays"
  1. […] by default knows how to use .get() on simple objects and arrays, but as with cloning and iterators, there is a way to register a custom .get() function for other object […]

Leave a Reply