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.
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]
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
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); } });
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]
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.