Rethinking JavaScript Object Enumeration (original) (raw)
In JavaScript, enumeration across regular (non-Array) Objects is often more painful than it should be. Arrays are merrily dispatched through for
and while
loops using all manner of crazy, fun techniques; Objects are forever at the mercy of the pedestrian, one directional for-in
loop, without which we can’t even learn the names and length of its own property set. Arrays have access to a plethora of elegant higher order functions (forEach
, map
, filter
etc.); Objects don’t. Until now, that is.
Borrowing from Prototype.js, ECMAScript 5 defines two nifty new methods Object.keys(obj) and the rather clunkily named Object.getOwnPropertyNames(obj). They already work in the current versions of Chrome and Safari and will be supported in Firefox 4 and IE9.
Object.keys(obj)
This method returns an array of all enumerable property names defined by a given object (inherited properties are not considered). Note that the sequence is based on the default for-in
looping sequence which may vary slightly between browsers (for full details on for-in
sequence see this article):
//Chrome, Safari, FF4, IE9 var purchases = {butter: 3.00, soap: 5.95, pineapple: 3.50 };
Object.keys(purchases); //['butter', 'soap', 'pineapple']
Now we can iterate an object’s properties in any sequence using a for
loop…
//Chrome, Safari, FF4, IE9 var keys = Object.keys(purchases), totalCost = 0;
for (var i=keys.length; i--;) { totalCost += purchases[keys[i]]; }
totalCost; //12.45
…or a while
loop…
//Chrome, Safari, FF4, IE9 var keys = Object.keys(purchases), i=keys.length, totalCost = 0;
while (i--) { totalCost += purchases[keys[i]]; }
totalCost; //12.45
For those browsers that don’t yet implement Object.keys
we can apply the following shim (thanks to @jdalton for reminding me to add type checking) :
//all browsers if (typeof Object.keys != 'function') { Object.keys = function(obj) { if (typeof obj != "object" && typeof obj != "function" || obj == null) { throw TypeError("Object.keys called on non-object"); } var keys = []; for (var p in obj) obj.hasOwnProperty(p) &&keys.push(p); return keys; } }
Object.keys({a:1, b:2, c:3}); //['a', 'b', 'c']
Now its easy to use an Object with one of the higher order iterators supplied by Array.prototype…
var thing = { size: 14, color: 'kind of off-white', greet: function() {return "thronk"} };
var thingFunctions = Object.keys(thing).filter(function(e) { return typeof thing[e] == 'function' });
thingFunctions; //["greet"]
…and we can use the map
function to create an Object.values
method too (because you know Harmony will be adding it any minute now 😉 )
Object.values = function(obj) { return Object.keys(obj).map(function(e) { return obj[e] }); }
Object.values({a:1, b:2, c:3}); //[1, 2, 3]
Object.getOwnPropertyNames(obj)
This one is a gem. Its similar to Object.keys but additionally returns the names of non-enumerable properties (again, inherited properties are not included). Now, at long last, you can list the properties of Math! The following snippet collects every Math function that expects exactly one argument and invokes it, passing the number 10…
//Chrome, Safari, FF4, IE9
Object.getOwnPropertyNames(Math).forEach(function(e) { if((typeof Math[e] == 'function') && (Math[e].length == 1)) { console.log("Math." + e + "(10) -> " + Mathe); } }); //Math.cos(10) -> -0.8390715290764524 //Math.log(10) -> 2.302585092994046 //Math.tan(10) -> 0.6483608274590867 //Math.sqrt(10) -> 3.1622776601683795 //etc...
…and here’s an array of all the properties of String.prototype…
//Chrome, Safari, FF4, IE9
Object.getOwnPropertyNames(String.prototype); //["length", "constructor", "concat", "localeCompare", "substring", "italics", "charCodeAt", "strike", "indexOf", "toLowerCase", "trimRight", "toString", "toLocaleLowerCase", "replace", "toUpperCase", "fontsize", "trim", "split", "substr", "sub", "charAt", "blink", "lastIndexOf", "sup", "fontcolor", "valueOf", "link", "bold", "anchor", "trimLeft", "small", "search", "fixed", "big", "match", "toLocaleUpperCase", "slice"]
Unlike Object.keys
we can’t replicate Object.getOwnPropertyNames
using regular JavaScript since non-enumerable properties are out of bounds when using traditional iteration loops. Check out this log for an insight into the hazards encountered during the webkit implementation.
A word on TypeErrors
EcmaScript 5 is making gestures towards limiting auto-coercion, notably with the introduction of Strict Mode. That effort also extends to most of the new methods introduced on Object
, including Object.keys
and Object.getOwnPropertyNames
. Neither method will coerce primitive arguments into Objects – in fact they will both throw a TypeError
:
//Chrome, Safari, FF4, IE9
Object.keys("potato"); //TypeError: Object.keys called on non-object
Object.getOwnPropertyNames("potato"); //TypeError: Object.getOwnPropertyNames called on non-object
Thus, the following examples represent one of the few scenarios outside of Strict Mode where it makes sense to use the new String
construction. Note that when either method is passed a string, the index name of each character is included.
//Chrome, Safari, FF4, IE9
Object.keys(new String("potato")) //["0", "1", "2", "3", "4", "5"]
Object.getOwnPropertyNames(new String("potato")) //["0", "1", "2", "3", "4", "5", "length"]
Wrap Up
Once they are available across all the major browsers Object.keys
and Object.getOwnPropertyNames
will make object/hash manipulation leaner and more powerful by plugging a major hole in the JavaScript Object API. Moreover as the line between Arrays and regular Objects blurs (aided by custom getters and setters) we’re likely to see a growth in generic “array-like” objects which enjoy the best of both worlds – non-numeric identifiers and access to the rich API set defined by Array.prototype. EcmaScript 5 has apparently pre-empted this trend by introducing the generic method, defined by one type but useable by any.
There’s a seismic shift under way – be ready for it!
Further Reading
ECMA-262 5th Edition
Object.keys(obj)
Object.getOwnPropertyNames(obj)