2
\$\begingroup\$

I have developed the following method for each as a replacement for $.each, and I am considering using it in my personal lightweight js library called LIBJS.

The final version will be part of a javascript class as opposed to a stand alone function.

  /**
   *
   * @method     each
   * @param      {Object}    obj        { Either an obj array or an object literal to be searched}
   * @param      {Function}  fn         { The method that has access to the searched value if exists }
   * @param      {Function} condition [condition=false]  { The condition that is used to filter the object or array }
   * @return     {Number}    { The number of matches found in the given obj or false if none found }
   */

  function each(obj, fn, condition) {



      var matches;

      if (typeof obj !== 'object') return; // Filter only objects and object arrays

      function eachObj(obj, fn, condition) {

          function parseAll() {
              for (var key in obj)
                  if (obj.hasOwnProperty(key)) fn(obj[key], key);
          }

          function parseConditional() {
              for (var key in obj)
                  if (obj.hasOwnProperty(key) && condition(obj[key], key)) {
                      fn(obj[key], key);
                      ++matches;
                  }
          }

          typeof condition === 'function' ? parseConditional() : parseAll();
          return matches;
      }

      function eachArr(obj, fn, condition) {

          if (!condition) obj.filter(fn);
          else obj.filter(function(idx, val) {
              if (condition(idx, val)) fn(idx, val);
          });

          return matches;

      }

      return Array.isArray(obj) ? eachArr(obj, fn, condition) : eachObj(obj, fn, condition);
  }

I feel that this approach is better than John Resigs $.each because:

It works on nodeLists, Arrays and normal Objects using the exact same API. I feel that this makes it more intuitive because regardless of the data structure, the developer can use the same call for the same overall purpose.

I allow the programmer to use the Array.prototype.filter style signature and logic but with an object! i noticed that the primary reason I loop through objects is the either do a comparison, make a modification or read a value, etc.

The method call when organized this way allows complete separation of the filtering logic from the logic that will be applied to the matching value or key. Furthermore, absence of the filter function will result in traditional $.each behavior (all members of the collection would be iterated as opposed to conditionally applied).

Additionally, I have included my $.find and $.replace replacements are powered by each as I imagine the rest of my lib will be as well. This is just to give more idea on how the each is implemented.

function find(subject, searchTerm, method, lastMatchCt){

    var totalMatches = lastMatchCt || 0;

    function searchEach(subject, searchSubj, method, lastMatchCt){
        each(subject, function(val, key){

            if (typeof val === 'object' && !Array.isArray(val)) {
                searchEach(val, searchSubj, method, totalMatches);
            }
            else if (val === searchSubj){
                method(subject, key);
                ++totalMatches;
            }
        }, function(val){

            if (val === searchTerm || typeof val === 'object') return true;

        });
        return totalMatches
    }

    return searchEach(subject, searchTerm, method, lastMatchCt);
}


function replace(source, search, replacement, customFilter){
    var filter = customFilter || function(objLoc, key){
        objLoc[key] = replacement;
    };
    return find(source, search, filter);
}

Last but not least, my version of each() returns the number of matches found when the dev uses the conditional function parameter.

My basic idea was to make a method that allows you to track and iterate ANY type of collection. Do you think that this each is more useful/better than John Resigs $.each? Am I missing something that John Resigs implementation included? Are the any performance/efficiency optimizations that could be introduced?

\$\endgroup\$
1
\$\begingroup\$

I feel that this makes it more intuitive because regardless of the data structure

Not really. It's when a function tries to do everything that makes it hard to debug. Baking in a filter makes it even more monolithic. I'd rather have explicit code than something that tries to do everything.

It works on nodeLists, Arrays and normal Objects using the exact same API. ... track and iterate ANY type of collection.

In JS, everything is an object. Does that mean it loops through everything? Also, the web is full of array-like structures, like arguments, NodeList, HTMLCollection, etc. Does it account for those as well? You'll be playing cat-and-mouse if you try to.

Last but not least, my version of each() returns the number of matches found when the dev uses the conditional function parameter.

Not sure why you'd want to do this. If I wanted to loop through, I wouldn't expect a return value, just looping through. The same behavior can be found in $.each, array.forEach, _.each and other variations.

What you're trying to do is just meld in _.forEach, _.toPairs, _.filter (from Lodash, Underscore has similar functions of different name) and length. It's not that bad when using chained calls.

// Loop through object and get length
_(obj).toPairs().value().length;

// Loop through array, filter and loop thtough
_(array).filter(condition).forEach(callback);

Native isn't as different either:

// Loop through object and get length
Object.keys(obj).length

// Loop through array, filter and loop thtough
array.filter(condition).forEach(callback);

Anyways, maintainability aside...

Your API calls itself each, but tries to do N number of things. I suggest you name it verbosely if you want to keep the functionality. Something like forEachFilterByOptionalConditionAndReturnLength. That sounds about right.

Array.isArray(obj)

You claim that you can loop through any collection. However, your condition doesn't seem to take this into account. NodeList will not pass your isArray test and will fall in the object loop. However, NodeList is an array-like structure. It's best looped through like an array. Consider converting it into an array using slice.

fn(idx, val)

As far as I know, only jQuery uses this convention where the key/index comes first. All the other library forEach as well as the native forEach have values come first, index come second.

for (var key in obj)
  if (obj.hasOwnProperty(key)...

// to

Object.keys(obj).forEach(function(key){
  var currentItem = obj[key];
  ...
});

Instead of for-in, use Object.keys. Essentially we're turning the object into an array of its own enumerable keys, just like a for-in and hasOwnProperty do. Seeing you're using filter in the array portion, you should be aware this is possible.

Again, I'd suggest not making a monolith. Keep your functions simple and verbose. The last thing we want to happen is lose hair during debugging.

\$\endgroup\$
  • 1
    \$\begingroup\$ +1 - was just writing most of those things myself, but your answer covers a lot more ground. Especially the order of arguments (idx and val) caught my eye, since they're being named such in filter callback - and filter callbacks (like forEach callbacks) receive their arguments the other way around. Sure, the internal naming is essentially irrelevant to the outside world, since it's up to the callbacks to handle them, but it strongly implies that OP has made a mistake. \$\endgroup\$ – Flambino Mar 29 '16 at 12:46
  • \$\begingroup\$ Thanks for the feedback! I use the val before the index because many times the index is not needed, only the value. This way, I make the optional parameter last. \$\endgroup\$ – Alpha G33k Mar 29 '16 at 22:10
  • \$\begingroup\$ Also good point about the nodeList. It DOES work fine still however because it slips into the for in loop. Just a question, I know for in for Arrays is not good practice however what about nodeLists and CSSStyleDeclorations and other Array like structures? \$\endgroup\$ – Alpha G33k Mar 29 '16 at 22:11
  • \$\begingroup\$ One more thing, my method is only a monolith if you add the filter parameter. Without passing this it is a normal each except it does not require. I still feel like more than 50% of the time when you use for in on an object or forEach on an array, you tend to DO something with a value in the collection. I understand what you are saying though... but over 50% of the time is compelling still IMO \$\endgroup\$ – Alpha G33k Mar 29 '16 at 22:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.