简体   繁体   中英

How to simplify and speed up _.each?

The native Array.prototype.forEach() is very slow. I don't know why. Also, I don't know why it is implemented in Underscore.js 's _.each() . I think people generally think that if it is in a library, or it is implemented by a browser, that it is correct/efficient until proven not correct/efficient.

Here is the proof:

http://jsperf.com/native-vs-implmented-0

If we simply remove the native call from underscore, _.each , we get:

  var each = _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (obj.length === +obj.length) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      for (var key in obj) {
        if (_.has(obj, key)) {
          if (iterator.call(context, obj[key], key, obj) === breaker) return;
        }
      }
    }
  };

I plan on asking the underscore team as well, but I wanted to verify I did not miss anything obvious.

Is this a valid reduction that will enhance performance? Is the performance hit taken in the hope that one day it will be faster? Can this reduction be made to enhance performance without losing useful functionality?

Take a look at Lo-Dash . The author has done a lot of work looking into these performance issues, especially in this blog post and this video .

If performance is your goal, you may be best served by writing your own iterator that does only what you need and nothing else—then it should perform very well. For example, in many cases you only need the array element and not the index. You definitely don't need to use .call() to pass the array element to the callback as this , since it's being passed as a named argument too. And do you really need to make return false; in the callback terminate the loop? I can't remember the last time I used that. So it could be as simple as:

function eachElement( array, callback ) {
    for( var i = 0, n = array.length;  i < n;  ++i ) {
        callback( array[i] );
    }
}

It's going to be hard to beat that for speed.

You can have a separate version when you need the index:

function eachElementAndIndex( array, callback ) {
    for( var i = 0, n = array.length;  i < n;  ++i ) {
        callback( array[i], i );
    }
}

You might use shorter function names; I just used these long names for clarity here. And it may turn out that eachElementAndIndex() is fast enough, so you might just call it each() and be done with it.

It's worth noting, as @YuryTarabanko points out in a comment above, that these simplified iterators don't meet the specification for Array.prototype.forEach() . In addition to not passing the array element as this and not passing the entire array as a third parameter, they don't check for missing elements and will call the callback for every array index from 0 through array.length - 1 regardless of whether the array elements actually exist. For example.

In fact, Underscore.js's _.each() is inconsistent in this regard. When it falls back to Array.prototype.forEach() , it skips missing elements, but when it uses its own for loop it includes them.

Try going to underscorejs.org and paste this into the Chrome console:

function eachElementAndIndex( array, callback ) {
    for( var i = 0, n = array.length;  i < n;  ++i ) {
        callback( array[i], i );
    }
}

function log( e, i ) {
    console.log( i + ':', e );
}

var array = [];
array[2] = 'two';

console.log( 'eachElementAndIndex():' );
eachElementAndIndex( array, log );

console.log( '_.each() using native .forEach():' );
_.each( array, log );

console.log( '_.each() using its own loop:' );
var saveForEach = Array.prototype.forEach;
delete Array.prototype.forEach;
_.each( array, log );
Array.prototype.forEach = saveForEach;

console.log( 'Done' );

It logs:

eachElementAndIndex():
0: undefined
1: undefined
2: two
_.each() using native .forEach():
2: two
_.each() using its own loop:
0: undefined
1: undefined
2: two
Done

In many (probably most) cases, missing elements don't matter. Either you're working with an array you created, or a JSON array, and you know it doesn't have any missing elements. For JSON arrays in particular, this is never an issue, because JSON doesn't have a way to make an array with missing elements. A JSON array can have null elements, but `.forEach() includes those like any other.

As long as your simplified iterator does what you want and isn't named Array.prototype.forEach , you don't have to worry about it meeting any specification other than your own. But if you do need to skip over missing array elements, take that into account in your own code or use the standard .forEach() .

Also, just to simplify the Underscore code you're looking at a bit further, since we're talking about .forEach() , that means we're only talking about the Array portion of _.each() , not the Object portion. So the actual code path of interest is just this:

  var each = _.each = _.forEach = function(obj, iterator, context) {
    if (obj == null) return;
    if (obj.length === +obj.length) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    }
  };

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM