Problem
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?
Solution
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.