JavaScript – Better Array Builder

Yesterday I wrote this post about a way to loosely imitate having array comprehensions in JavaScript. After thinking about it a bit more, I thought it would be best to change the function to be more like this:

/**
 * @license Array Builder v2 - By Chris West - MIT License
 * Builds an array either from another array or an object using the values.
 * @param {Array|Object|number} obj  If an array or object is given all of its
 *     items will be traversed.  If a number is given, an array with that length
 *     will be created and then traversed.
 * @param {string|Function|undefined} expression  Optional function or function
 *     expression that if the filter passes will be executed and the returned
 *     value will be placed in the array to be returned by this function.  If a
 *     string is specified, the context object (this) will be obj, the value
 *     will be $value and the key will be $key.  If a function is specified, the
 *     context object (this) will be obj, the key will be the first parameter
 *     and the value the second parameter.  If not specified and obj is number
 *     the value to be added to the returned array will be the key.  If not
 *     specified and obj is not a number, the value to be added to the returned
 *     array will be the value found.
 * @param {string|Function|undefined} optFilter  Optional function or function
 *     expression that will be evaluated for each value and only if a true-ish
 *     value results will the expression be evaluated and the resulting value
 *     added to the built array.  If a string is specified, the context object
 *     (this) will be obj, the value will be $value and the key will be $key.
 *     If a function is specified, the context object (this) will be obj, the
 *     key will be the first parameter and the value the second parameter.
 * @return {!Array}  The array built from the values returned from expression.
 */
var buildArray = (function(toString, undefined) {
  // Internal function which checks the type of an object or primitive
  // (excluding null and undefined).
  function typeIs(obj, typeName) {
    return toString.call(obj).slice(8, -1) == typeName;
  }

  return function(obj, expression, optFilter) {
    // If a number is passed instead of an array or object, create a blank array
    // with that length.
    var objWasNumber = typeIs(obj, 'Number');
    if (objWasNumber) {
        obj = new Array(obj);
    }

    // Make sure the expression is a function.
    if (expression == undefined) {
      expression = objWasNumber ? '$key' : '$value';
    }
    if (typeIs(expression, 'String')) {
      expression = new Function('$value', '$key', 'return ' + expression);
    }

    // If given make sure the filter is a function.
    if (optFilter && typeIs(optFilter, 'String')) {
      optFilter = new Function('$value', '$key', 'return ' + optFilter);
    }

    // This is called for each item in obj and is used to execute the filter (if
    // given) and then add the processed value to the array to be returned.
    function process(key) {
      var value = obj[key];
      if (!optFilter || optFilter.call(obj, value, key)) {
        ret.push(expression.call(obj, value, key));
      }
    }

    // Create the array, build it out, and return it.
    var ret = [];
    if (typeIs(obj, 'Array')) {
      for (var i = 0, len = obj.length; i < len; i++) {
        process(i);
      }
    }
    else {
      for (var key in obj) {
        process(key);
      }
    }
    return ret;
  };
})({}.toString);

Reasons for the Change

  • I wanted the ability to specify a number as the first parameter which would in turn create an array of that length and then process the array.
    buildArray(26, 'String.fromCharCode($key + 65)')
    // Returns array of the letters in the alphabet capitalized.
    
  • I wanted to remove the declaration of a variable which will be used to specify the name of the value.
  • I wanted the key to also be passed into the expression and filter functions.
    buildArray({ firstName: 'Chris', lastName: 'West' }, '$key')
    // ['firstName', 'lastName'] - Not necessarily in this order.
    
  • I wanted to be able to reference the object or array being examined.
    buildArray(['A', 'B', 'C', 'D', 'E'], 'this[this.length - $key - 1]')
    // ['E', 'D', 'C', 'B', 'A']
    
  • I wanted a way to mimic this call in python: range(10).
    buildArray(10)
    // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    

In conclusion there are many advantages to making these changes to the buildArray function. Of course, it still falls short of Python list comprehensions and true array comprehensions, but in the absence of such elegance we must resort to hacks. 😎

Leave a Reply

Your email address will not be published. Required fields are marked *