Array Processing

Arrays are one of the most fundamental data structures in any imperative programming language, including JavaScript. Unfortunately, however, standardized array operations are not supported by all mainstream browsers, and as long as that is the case, it's immensely helpful to have a toolkit that protects you from the bare metal. For that matter, even if the next version of each major browser supported arrays in a completely uniform manner, there would still be way too many people out there using older browsers to begin thinking about going back to the bare metal anytime soon.

Tip

You may find it interesting that the various language tools have been optimized for performance, providing wrapped usage of the native Array implementations wherever possible, but emulating functionality for browsers like IE when it is missing.

Fortunately, Dojo strives to keep up with Mozilla's feature rich implementation of the Array object (http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference). As long as you have the toolkit with you, you'll never be caught defenseless again. And in case you have already forgotten from our discussion of dojo.byId in Chapter 1 that you really can't take much for granted in the current browser ecosystem, the next section should reinvigorate your enthusiasm and might even surprise you.

Finding Locations of Elements

Two very routine array operations involve finding the index of an element, which is really one and the same as determining if an element exists at all. Base facilitates this process with two self-explanatory operations, dojo.indexOf and dojo.lastIndexOf. Each of these functions returns an integer that provides the index of the element if it exists; the value -1 is returned if the element was not found at all. These function signatures also include an additional parameter that indicates the value that should be used as an initial location in case you don't want to start from the very beginning or end of the array. The signature is the same for each function:

dojo.indexOf(/*Array*/ array, /*Any*/ value, /*Integer?*/ fromIndex)
//returns Integer

dojo.lastIndexOf(/*Array*/ array, /*Any*/ value, /*Integer?*/ fromIndex)
//returns Integer

Warning

If you've been primarily developing with Firefox for a while, you may be surprised to learn that native Array objects in IE6 and IE7 do not even support the indexOf method. Unfortunately, this kind of semantic misunderstanding about something that may seem so obvious can be one of the hardest kinds of bugs to track down.

The following code snippet illustrates some basic usage of these methods:

var foo = [1,2,3];
var bar = [4,5,6,5,6];
var baz = [1,2,3];

dojo.indexOf([foo, bar], baz); // -1
dojo.indexOf(foo, 3); // 2
dojo.indexOf(bar, 6, 2); // 2
dojo.indexOf(bar, 6, 3); // 4

dojo.lastIndexOf(bar, 6); // 4

A more subtle point about these methods is that they perform shallow comparisons, which in the case of complex data types like Array, means that the comparison is by reference. The following snippet clarifies with a concrete example:

bop = [4,5,6,5,6, foo]; // bop contains a nested Array
dojo.indexOf(bop, foo); //5, because (a reference to) foo is contained in bop
dojo.indexOf(bop, [1,2,3]); //-1, because foo is not the same object as [1,2,3]

Testing Elements for a Condition

It is often the case that you may be interested in knowing if each element of an array meets a particular condition, or if any element of an array meets a particular condition. Base provides the every and some functions for performing this kind of testing. The input parameters are the array, a function that each element of the array is passed into, and an optional parameter that can supply the context (this ) for the function:

dojo.every([2,4,6], function (x) { return x % 2 == 0 }); //true
dojo.every([2,4,6,7], function (x) { return x % 2 == 0 }); //false

dojo.some([3,5,7], function f(x) { return x % 2 == 0 }); //false
dojo.some([3,5,7,8], function f(x) { return x % 2 == 0 }); //true

Iterating Over Elements

The forEach function passes each element of the array into a function that takes up to three parameters and does not return any value at all. The first parameter is the current element of the array being iterated over, the second (optional) parameter is the index of the current element within the array, and the third (optional) parameter is the entire array itself. The forEach function is generally used to iterate over each element of an array as an ordinary for loop. Here's the signature:

dojo.forEach(/*Array*/ array, /*Function*/ function) // No return value

In its simplest form forEach works like so:

dojo.forEach([1,2,3], function(x) {
  console.log(x);
});

Some obvious benefits of forEach is that it introduces less clutter than explicitly introducing a for loop and requiring you to manage a counter variable and also allows you to introduce the Array variable inline. However, perhaps the most important thing that it does is leverage the closure provided by the function as the second parameter to protect the immediate context from the counter variable and other variables that may be introduced in the loop's block from persisting. Like other utility functions, forEach provides an optional parameter that can supply the context for the inline functions.

To illustrate how forEach can save you from unexpected consequences, consider the following snippet of code:

var nodes = getSomeNodes(  );

for(var x=0; x<nodes.length; x++){
    nodes[x].onclick = function(  ){
        console.debug("clicked:", x);
    }
}

Which value of "x" would you expect here? Since the enclosure is over the lexical variable x and not the value of x, all calls get the last value. forEach gets us out of this handily by creating a new lexical scope. This variation illustrates how to iterate over the array and produce the expected value:

var nodes = getSomeNodes(  );
var idx = 0;
dojo.forEach(nodes, function(node, idx){
    node.onclick = function(  ){
        console.debug("clicked:", idx);
    }
});

Transforming Elements

While the map and filter functions have the same function signature as forEach, they're very different in that they apply some custom logic to each element of the array and return another array without modifying the original one.

Warning

While you could technically modify the original array through the custom map and filter functions, it's generally expected that map and filter will be free of side effects. In other words, introduce side effects with a lot of discretion and an explicit comment saying as much.

As programmers from functional programming languages (or even programming languages with functional extensions like Python) know all too well, map and filter grow on you quickly because they provide so much functionality with such concise syntax.

The map function might almost sound mysterious if you haven't encountered it before; it's actually named self-descriptively because it builds a mapping from the array you give it via a transform function. The following example illustrates:

var z = dojo.map([2,3,4], function(x) {
  return x + 1
}); //returns [3,4,5]

For comparison purposes, consider how you might compute the value for z in the example above without the map function:

var a = [2,3,4];
var z = [];
for (var i=0; i < a.length; i++) {
  z.push(a[i] +1);
}

Like forEach, one of the benefits of using map directly is that the overall expression is clearer, resulting in more maintainable code. You also get the same kind of anonymous function benefit in that a closure surrounds the code block, whereas variables introduced via intermediate calculations would pollute the context without the closure.

The filter function is also a self-descriptive function in that it filters an array according to a function's criteria. Here it is at work:

dojo.filter([2,3,4], function(x) {
  return x % 2 == 0
}); //returns [2,4]

Implementing a block of equivalent code is relatively simple but does require more bookkeeping and clutter—and more opportunity for typos and bugs:

var a = [2,3,4];
var z = [];
for (var i=0; i < a.length; i++) {
  if (a[i] % 2 == 0)
    z.push(a[i]);
}

Like the other array functions provided by Base, you can also provide additional parameters that supply context for or map or filter if you need them:

function someContext(  ) { this.y = 2; }
var context = new someContext;
dojo.filter([2,3,4], function(x) {return x % this.y==0}, context); //returns [2,4]

String-As-Function Style Arguments

Base also provides the ability to create the shorthand "string-as-function" type arguments for the forEach, map, filter, every, and some functions. In general, this approach is less verbose than writing a function wrapper and is especially handy for really simple cases where you're doing a quick transform. Basically, you just provide a string value with the function body in it versus the entire function. Three special keywords have special context if they appear in the string:

item

Provides a reference to the item that is currently being processed

array

Provides a reference to the entire array that is being processed

index

Provides a reference to the index of the item that is currently being processed

Consider the following example, which demonstrates two equivalent approaches for achieving the same end:

var a = new Array(1,2,3,...);

//A lot of extra typing for very little purpose
a.forEach(function(x) {console.log(x);}); //approach one

//A lot less typing so that you can get work done quickly
a.forEach("console.log(item)"); //approach two

Warning

Using the shortened string-as-function approach to array-like methods can make your code more concise, but it may make it difficult to track down bugs, so use discretion. For example, consider the following variation of the previous code snippet:

var a = new Array(1,2,3,...);
a.forEach("console.log(items)"); //oops...extra "s" on items

Because there's an extra "s" on the special term item, it won't act as the iterator anymore, effectively rendering the forEach method as a no-op. Unless you have an especially good eye for tracking down these types of misspellings, this could cost you debugging time.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.