Udostępnij za pośrednictwem


ECMAScript 5 Part 2: Array Extras

Last time in our series on IE9 support for ES5, we talked about new functions like Object.create and Object.defineProperty, which help you design more reusable components. In this post, we’ll look at another set of new functions focused on something even more basic and common: loops and arrays.

Looping over arrays is one of the most common forms of program control flow, and JavaScript programs in the browser are no exception. Common tasks such as finding a node in the DOM, iterating over the records in a JSON object from the server and filtering arrays of items based on user input, all involve iterating over arrays.

ES5 adds a set of new functions to make these common tasks easier and less error-prone. We’ll look at a few examples in this post, and see how these new functions can enable a more functional or declarative style of programming.

For Each

Have you ever tried to write JavaScript code like the following?

 var people = ["Bob", "Jane", "Mary", "Chris"];

for(var person in people) {
  processPerson( person );
}

If so, you’ve probably noticed that for..in loops give you the indexes of the elements in the array, not the elements themselves. Instead, JavaScript programmers usually write this as:

 for(var i = 0; i < people.length; i++) {
  processPerson(people[i]);
}

This is a very common pattern and ES5 introduces a new Array function, Array.prototype.forEach to make this really easy:

 people.forEach(processPerson);

Using forEach makes this code a little shorter, but there are other more important reasons why it is often a better choice. First, you don’t have to write the array iteration logic yourself, which means fewer opportunities for bugs, and in this case less chance of an accidental off-by-one error or incorrect length calculation. Second, forEach handles arrays with empty elements automatically, skipping those indexes that don’t have values. And third, putting the body of the loop into a function ensures that any variables defined will only be in scope within the loop body, reducing the risk of loop variables conflicting with other variables in your code.

The forEach function gives a name to a common pattern, and provides a convenient way to iterate over arrays. You can see some examples of forEach in action in the ES5 Tile Switch Game on the Internet Explorer Test Drive site.

Other Common Loops

There are some other common patterns of looping over arrays. Here’s one you’ve probably seen before:

 var newData = []
for(var i = 0; i < data.length; i++) {
  var value = data[i];
  var newValue = processValue(value);
  newData.push(newValue);
}

Here we create a new array by transforming each value from an existing array. Again, there’s a lot of boilerplate logic here. Using the ES5 Array.prototype.map function, we can write this more simply as:

var newData = data.map(processValue)

Here’s another common pattern:

 var i;
for(i = data.Length - 1; i >= 0; i--) {
  if(data[i] === searchValue) 
    break;
}

This is the sort of code that is used to search for the last index of a value in an array – in this case searching backward from the end. Using the ES5 Array.prototype.lastIndexOf function, we can express this:

var i = data.lastIndexOf(searchValue)

The common theme here is that there are a number of common patterns of looping over arrays, and the new ES5 Array functions make it easier, more efficient, and more reliable to express these.

Aggregation

Another common use of loops is to reduce an array of data into a single value. Frequent instances of this are summing a list, getting the average of a list, and turning a list into a nicely formatted string.

The example below loops through a list to compute the sum of its values:

 var sum = 0;
for(int i = 0; i < data.length; i++) {
  sum = sum + data[i];
}

With the new Array.prototype.reduce: function in ES5, we no longer have to keep track of the loop index and can aggregate the operation over the array.

var sum = data.reduce(function(soFar, next) { return soFar + next; })

In this example, the function passed to reduce is called once for each element of the array, each time being passed the sum so far, as well as the next element.

The reduce function can also take an extra parameter when there is an initial value for the aggregation. This optional parameter can also be used to keep track of multiple pieces of state. The following computes the average of an array of numbers:

 var result = 
  data.reduce(function(soFar, next) { 
    return { total: soFar.total + next, 
             count: soFar.count + 1   }; 
    },
    {total: 0, count: 0 }
  );

var mean = result.total / result.count;

To process the array in descending order, from last element to first element, use the Array.prototype.reduceRight function.

Another form of array aggregation is checking whether every (or at least some) element of the array satisfies a specific requirement. The Array.prototype.every and Array.prototype.some functions provide an easy way to check these conditions. An important characteristic of these aggregates is that they can short-circuit – the result might be known before all the elements of the array have been checked, and the loop will exit.

 var allNamesStartWithJ = 
  data.every(function(person) { return person.name[0] === 'J'; })

As with the example in previous sections, reduce, reduceRight, every, and some make writing loops to aggregate lists easier and less error prone, while also being more composable, as we’ll see in the next section.

Putting Them Together

A few of the functions described above start with an array and produce a new transformed array. This approach allows you to compose the functions together, and to write some nicely declarative code for transforming arrays of data.

Here’s an example of using the filter, map, and reduce functions together. Imagine we are building a site that processes transaction records from cash registers. These transaction records may contain comma-separated entries indicating purchases (‘P’), refunds (‘R’) or cancelled transactions (‘C’). Sample data might look like this:

var transactions = "P 130.56, C, P 12.37 , P 6.00, R 75.53, P 1.32"

We can calculate the sum of all purchases by combining some of the array functions we’ve seen:

 transactions
  // Break the string into an array on commas
  .split(",")
  // Keep just the purchase transactions ('P')
  .filter(function(s) { return s.trim()[0] === 'P' })
  // Get the price associated with the purchase
  .map   (function(s) { return Number(s.trim().substring(1).trim()) })
  // Sum up the quantities of purchases
  .reduce(function(acc, v) { return acc + v; });

This style of programming is commonly referred to as functional programming and is common in languages like Lisp and Scheme, both of which influenced the original design of JavaScript. This style is also related to concepts like Language Integrated Query (LINQ) in .NET, and the Map-Reduce model for processing and generating very large datasets in distributed computing.

The New ES5 Array Functions

In total, there are nine new functions for searching and manipulating arrays in ES5, all supported in IE9:

Each of these functions also supports additional optional parameters not shown in the examples above, which increases their flexibility. Also, these functions can be applied not just to arrays, but to any JavaScript object that has an integer-named index and a length property.

You can try these array functions out yourself in the ECMAScript 5 Arrays demo on the IE9 Test Drive site.

The functions covered in this post are just one of the ways you can use ES5 to write simpler, more reusable code. Remember, to leverage these features, the browser must be in IE9 Standards Document Mode which is the default in IE9 for a standard document type. We’re excited to see how developers will use the features of ES5 with IE9; we’d like to hear how you plan to put these features to use. We’ll continue to explore the additions to ES5 in upcoming posts. Stay tuned!

Luke Hoban

Program Manager, JavaScript

Update 12/15 – small edit in the 2nd line of the last code sample in the Aggregation section.

Comments

  • Anonymous
    December 13, 2010
    On the topic of Javascript, I have this simple function that moves an absolute positioned div block to the right: var a = 0; var c = 32; function lala(){ document.getElementById('brix').style.left = c + 'px'; if (c>300) {clearTimeout(a);} else {c=c+1;} var a = setTimeout(lala,1); } With this code, the div crawls very slowly, even in the latest IE9 platform preview. On other browsers, the movement is much faster and snappier... what could be the reason for this?

  • Anonymous
    December 13, 2010
    @Arieta How about using setInterval() instead, and doing some caching of the left property... like: var c = 32; var p = document.getElementById('brix').style.left; var a = setInterval(function() { p = c + px; if (c>300) {clearInterval(a);} else { c++; } } ,1); I'm guessing (just guessing... I haven't done extensive benchmarks) the creation of new timeouts is just not as optimized in IE9 as in other browsers... with setInterval, it's like you're preparing all timeouts at once.

  • Anonymous
    December 13, 2010
    IE Team, what about implementing ES5 Strict mode? Firefox 4 and WebKit nightly builds support them.

  • Anonymous
    December 13, 2010
    @Arieta "With this code, the div crawls very slowly, even in the latest IE9 platform preview. On other browsers, the movement is much faster and snappier... what could be the reason for this?" Because this behavior is not tested by popular JS benchmarks. :p

  • Anonymous
    December 13, 2010
    I hate to repeat this here but once a post on the IE blog is not the latest post it gets ignored. Can someone from Microsoft please make a statement about shutting down the IE6/IE7/IE8/IE9 images at http://www.spoon.net/ ====================================================================================================== This was THE most useful resource for testing multiple versions of IE and the shutdown really ticked developers off! As a long time web developer of Enterprise Web Applications I've tried all the options out there to try and simplify testing IE and the lack of realistic options is a royal PITA. 1.) Multiple IEs - IE8 breaks the functionality of IE6's textboxes - thus its a NO-GO 2.) IETester - works great until you need to test popup interaction and then it fails - thus a NO-GO 3.) Virtual PC with timebombed images of IE6, IE7, IE8 - works ok, but the 12Gigs of HD space needed is frustrating when each full image of Windows dies 4 times a year, running a full Windows image is slow and you have to beg for updates because the releases are not co-ordinated and announced well at all - thus its a NO-GO 4.) IE Super Preview - Last I checked this did not allow full testing of IE user interaction, JavaScript DOM changes, popups etc. - thus its a NO-GO 5.) Multiple PC's to run multiple versions of windows and IE.  With all the hardware, software, and physical space needed - its a NO-GO 6.) Spoon.net IEs - They work, they work just like local native apps once running, and there's no hacking of my real local IE install. - the ONLY problem with these IE's is that Microsoft shut them down Please understand that we (developers) just want something that works.  Testing in multiple versions of IE is a pain to begin with and with IE9 on the horizon it is only getting worse. I'm not sure where the issue stands with Spoon, but I would really like a solution worked out fast. Steve

  • Anonymous
    December 13, 2010
    Can you put URL hyperlinks in to the ACTUAL ECMAScript 5 Array Methods? www.ecmascript.org/.../tc39-2009-043.pdf MSDN documentation is NOTORIOUS for NOT implementing according to the specs, and/or implementing features beyond the specification "claiming" that this implementation matches the spec, and worse yet never being updated to correctly identify obvious errors and incorrect implementations.

  • Anonymous
    December 13, 2010
    @Arieta: Given how much it takes to move the div to the final position (about 4 seconds) and the distance to move (268) I'd say that the timer used by setTimeout has a minimum resolution of 16ms. Try using setTimeout(lala, 16) and c = c + 16.

  • Anonymous
    December 13, 2010
    in the MSDN documentation there are code samples (why there are VB, C#, C++, & F# options is beyond me) that make use of JavaScript functions that start with an uppercase letter: e.g. function CheckIfEven(...), ShowResults(...), CheckIfPrime(...), CheckKey(...), CheckValue(...), Process(...), Process2(...), AppendToArray(...) Proper JavaScript naming conventions follow those of Java whereby functions should start lowercase, unless they are Object Constructors. Please fix this documentation ASAP.

  • Anonymous
    December 13, 2010
    There's a mistake in your every() example, which should return a boolean but it actually doesn't return the result of the comparison. var allNamesStartWithJ =  data.every(function(person) { return person.name[0] === 'J'; })

  • Anonymous
    December 13, 2010
    I love how IEBlog posts are about innovative annothing d just reports on how IE is finally slowly catching up on what other browsers have had for months and years. By the way: why is the address bar and the tabs all mushed together in IE9? It looks completely ridiculous. Are you just trying to be "different"? Sure, different is sometimes good, but in this case, it's just silly.

  • Anonymous
    December 13, 2010
    I love how IEBlog posts are about nothing innovative and just reports on how IE is finally slowly catching up on what other browsers have had for months and years. By the way: why is the address bar and the tabs all mushed together in IE9? It looks completely ridiculous. Are you just trying to be "different"? Sure, different is sometimes good, but in this case, it's just silly.

  • Anonymous
    December 13, 2010
    @Len, did you bother to compare the MSDN documentation and/or the IE 9 implementations with the specification?  It makes more sense to link to the docs here so people can see examples (which aren't provided in the specification) and navigate more easily.  So far, IE 9's ECMA 5 support has been implemented according to the spec and if it hasn't you should be providing feedback accordingly. Also, the document you linked was the final draft (September '09) of the standard before it was finalized.  The correct URL for ECMAScript 5th Edition (December '09) is www.ecma-international.org/.../ECMA-262.pdf.

  • Anonymous
    December 14, 2010
    But I remember seeing most of this stuff in firefox, I wanted to use it then and still want to use it now. But while users of windows XP are stuck with IE8 I can't use these functions directly.

  • Anonymous
    December 14, 2010
    boen_robot: I get an "object must be set" error with that. (in loose translation, since I'm not on an english OS)

  • Anonymous
    December 14, 2010
    @Arieta Sorry, I was just refactoring with no real testing... now that I tested, I saw I had two errors - first and foremost, the "left" is not cacheable, as it's a literal string. The style object of the "brix" element is cacheable though. And second, I forgot to add quotes around "px". So, with those two changes, the resulting code is: var c = 32; var p = document.getElementById('brix').style; var a = setInterval(function() { p.left = c.toString() + 'px'; if (c>300) { clearInterval(a); }else { c++; } } , 1); And at least on my computer, both IE9 and Firefox work at pretty much the same speed.

  • Anonymous
    December 14, 2010
    The comment has been removed

  • Anonymous
    December 14, 2010
    @Arieta It runs automatically... that's why you get the error - the element is selected before it's in the DOM, therefore the error. I'm assuming the onclick handler is for the lala() function? You can surround the whole thing with "function lala() { /my code above/ }", and it should work. (oh, and the ".toString()" part is redunant... I added it just in case, while trying to find my error, and forgot to remove it when posting the code here) BTW, on a closer inspection about the "visual" performance (no watches...), IE9 seems to do a smoother animation than Firefox, but a slower one (the end is reached a little later), even with setInterval(). Firefox is a little more choppy, but the animation itself is faster (the end is reached sooner). Is that what you had in mind earlier about IE being slower?

  • Anonymous
    December 14, 2010
    boen_robot: right, moving the script to the end of the document made it work. If I remove .toString I get an error, doesn't matter though. However, I still get IE being slower (reaching the end of the animation after the most time), Firefox being just a little faster, and Chrome is significantly faster than both. Kinda thinking of it, 32ms sounds like the normal minimum for 1px movement, since 1000 ms / 60 frames equals 32ms, so 32ms is the equivalent for one frame. Faster movement is only possible if I move more pixels per frame, which makes the animation choppier. This raises two questions: 1. are the browsers actually moving the animation at the speed specified, and 2. if they do, how does Chrome end up faster still?

  • Anonymous
    December 14, 2010
    Awesome. I write LOTS of loops over arrays in JS, so I'm looking forward to using these methods. ... Also, on an unrelated topic, does anyone know if IE9 will support web workers?

  • Anonymous
    December 14, 2010
    @Arieta 1000 / 60 = 16.... not 32. If you set the interval to 16, all browsers run at IE9's pace (and IE9 remains the one running most smoothly... or so it appears to me at least). It seems other browsers employ different techniques (with Opera's one being the best according to my tests, followed by Chrome) to make the animation appear faster, while IE9 schedules any interval lower than 16ms back to 16ms. I don't know about you, but I'm not holding my breath of an optimization on THAT in the IE9 time frame... but I'd hope there will be something in this area for IE10. Today, the best you can do is to make the interval of 16ms and make the step 16px. Visually, it will look as if you're moving 1px every 1ms... but at a speed no larger than 60fps (which is enough for a smooth animation)... and it will work equally fast in all of today's browsers.

  • Anonymous
    December 14, 2010
    The comment has been removed

  • Anonymous
    December 15, 2010
    @AndyEarnshaw:  Thanks for catching that bug in the code sample - we've updated the post with the correct code.

  • Anonymous
    December 15, 2010
    why are you using strict comparison in allNamesStartWithJ example? var allNamesStartWithJ =  data.every(function(person) { return person.name[0] === 'J'; })

  • Anonymous
    December 16, 2010
    @Andy Earnshaw - I'm not sure what happened to my previous comments but I've replied twice to your comment.  There are several differences and omissions in the MSDN documentation - many of which I found within a quick 60 second scan. In particular the MSDN documentation does not mention anything about what the filter.length or reduce.length property will return? (it should return 1) filter's and some's callbacks takes 3 parameters but if it is only passed 1 or 2, there are specific rules that it follows - the MSDN documentation makes no mention of this... in the past when MSFT made no mention of the specification behavior it usually means they decided to roll their own behavior and not declare that it differs from the spec. The point is simple - if there is an official spec, POINT TO IT!!!  do not point to your own mutation of the spec.  I'm tired of going to MSDN to see why JS methods are acting weird only to find out that the "Microsoft Implementation" differs from the spec... but doesn't indicate such. Drives me insane.

  • Anonymous
    December 19, 2010
    @Len: Array.prototype.filter.length and Array.prototype.reduce.length do return 1 in the Internet Explorer 9 previews.  The specification provides this as clarification for implementers for virtually every method - but all of them follow the behaviour outlined in section 15.3.5.1.  I would expect this behaviour to be outlined in its own section of the MSDN documentation (and it is, but in fairness it could be worded a little better to apply more generically to native and defined functions).  The majority of information in the ECMAScript specification is provided for implementers to follow, the fact that it is sometimes a useful reference for developers is pretty much a side effect.  As the majority of us readers are web developers and not implementers, a link to the documentation with several examples is infinitely more useful than a link to the specification that tells us what should be happening behind the scenes when we call a specific function.  However, I do concur that a single link to the specification would be appropriate alongside the links to the MSDN documentation for each method. Also, I think you're confusing the specification re: the callback functions.  The callbacks are always passed the expected arguments, whether specified or not.  The developer can still access those arguments through the arguments object, even if they weren't named in the callback function.