Enumerating JavaScript Objects
|
|
|
| 2.7/5.0 (3 votes total) |
|
|
|
Dean Edwards July 28, 2006
|
Dean Edwards |
This article appeared originally on UK programmer Dean Edwards' website, where he regularly posts quality JavaScript articles.
The article is licensed under the Creative Common's Licence. |
Dean Edwards
has written 1 articles for JavaScriptSearch. |
View all articles by Dean Edwards... |
Enumeration lies at the heart of DOM Scripting:
var lists = document.getElementsByTagName("UL"); for (var i = 0; i < lists.length; i++) { lists[i].className = "menu"; }
for (var i = 0; i < array.length; i++) { print(array[i]); }
for (var key in object) { print(object[key]); }
Us JavaScripters are forever writing loops like these. To ease the strain on our keyboards
Mozilla recently introduced a handy forEach method for arrays:
array.forEach(print);
If you don’t understand the code above then go and read the
documentation.
That’s fine for arrays but what about DOM node lists?
That’s where most of our loop writing is concentrated.
Fortunately, the clever Mozilla developers provide
generic array methods to help us:
var lists = document.getElementsByTagName("UL"); Array.forEach(lists, function(list) { list.className = "menu"; });
Cool huh? The Array.forEach method treats any object passed to it as if it were an array. So long as that object has a length property then we are OK.
We can easily implement this method for non-Mozilla browsers:
// array-like enumeration if (!Array.forEach) { // mozilla already supports this Array.forEach = function(object, block, context) { for (var i = 0; i < object.length; i++) { block.call(context, object[i], i, object); } }; }
I’ve been using this technique for enumeration quite a lot recently and I decided to extend the idea:
// generic enumeration Function.prototype.forEach = function(object, block, context) { for (var key in object) { if (typeof this.prototype[key] == "undefined") { block.call(context, object[key], key, object); } } };
// globally resolve forEach enumeration var forEach = function(object, block, context) { if (object) { var resolve = Object; // default if (object instanceof Function) { // functions have a "length" property resolve = Function; } else if (object.forEach instanceof Function) { // the object implements a custom forEach method so use that object.forEach(block, context); return; } else if (typeof object.length == "number") { // the object is array-like resolve = Array; } resolve.forEach(object, block, context); } };
This allows me to write loops without knowing what kind of object I’m dealing with:
function printAll() { forEach (arguments, function(object) { forEach (object, print); }); }; // or forEach (document.links, function(link) { link.className = "super-link"; }); // or forEach ([1, 2, 3], print); forEach ({a: 1, b: 2, c: 3}}, print); // etc
Explanation
The global forEach function allows us to enumerate any object according to it’s type.
If the object is array-like (has a length property) then we enumerate it like an array.
All other objects are enumerated using the standard for var x in y mechanism.
When enumerating over objects, the discovered keys are compared against Object.prototype .
If the key is defined on the Object object then it is not enumerated.
That means that you cannot enumerate the built-in methods like toString and valueOf .
The global forEach function will delegate the enumeration of functions to Function.forEach .
So, if you choose to enumerate over a Function object you will skip the built-in methods there too.
The Kick-Ass Bit
Although I’ve defined a forEach method on Function.prototype this is never called
by the global forEach function (except when you are enumerating functions).
I’ve provided this as a bonus feature.
Basically, by calling the forEach method on a function you can enumerate
an object and compare the keys with that function’s prototype.
That means that you will only enumerate custom properties of the object.
An example is required:
// create a class function Person(name, age) { this.name = name || ""; this.age = age || 0; }; Person.prototype = new Person;
// instantiate the class var fred = new Person("Fred", 38);
// add some custom properties fred.language = "English"; fred.wife = "Wilma";
Enumerate using the standard forEach method:
forEach (fred, print); // => name: Fred // => age: 38 // => language: English // => wife: Wilma
Enumerate using the Person.forEach method:
Person.forEach (fred, print); // => language: English // => wife: Wilma
Note that the properties defined on the prototype are not enumerated in the second example.
Conclusion
I’m totally in love with this technique. I now use it all the time.
It makes my code more readable and I feel safer enumerating over objects on different platforms.
It also saves on typing. |