1
\$\begingroup\$

I've written this relatively simple class to help with managing arrays of anonymous objects in my scripts.

/**
* AoHandler - accepts an array of objects or creates a new array
* @param {array} ao
*/
function AOHandler(ao)
{
    this.ao = typeof ao === 'object' ? ao : [];

    /**
    * pluck()
    * @param {object} search 
    * @param {boolean} retKey
    */
    this.pluck = function (search, retKey)
    {
        for (i = 0; i < this.ao.length; i++ )
        {
            var tmpObj = this.ao[i];

            if (tmpObj[search.key] == search.value)
            {
                return (retKey === true) ? i : tmpObj;
            }
        }
        return false;
    }
}

/**
* get()
* @param {object} search
* @return first matching object in ao array or false on failure
*/
AOHandler.prototype.get = function(search)
{
    return this.pluck(search);
}

/**
* add()
* @param {object} pushObj
* @return false on failure
*/
AOHandler.prototype.add = function(pushObj)
{
    return typeof pushObj === 'object' ? this.ao.push(pushObj) : false;
}

/**
* update()
* @param {object} search
* @param {object) updObj
* @param {boolean} merge
*/
AOHandler.prototype.update = function(search, updObj, merge)
{
    var key = this.pluck(search, true);

    if (merge === true && key !== false)
    {
        $.extend(this.ao[key], updObj);

        return true;
    }
    else if (key !== false)
    {
        this.ao[key] = updObj;

        return true;
    }

    return false;
}

/**
* remove()
* Finds the array item and removes it from the array
* @param {object} search
* @return first matching object in ao array
*/
AOHandler.prototype.remove = function(search)
{
    var key = this.pluck(search, true);

    return key !== false ? this.ao.splice(key, 1) : false;
}

Usage

var aoArray = new AOHandler();

aoArray.add({
    id : '50ef3ac',
    name : 'example',
});

aoArray.add({
    id : '50ef3ad',
    name : 'A new object'
});

var searchFor = {
    key : 'id',
    value : '50ef3ad'
}

console.log(aoArray.get(searchFor);

I'm unhappy with the one jQuery call with $.extend() but this proved the easiest way to accomplish an object merge and I use jQuery quite predominantly in the majority of my projects. Despite that can anyone suggest a different way to do this?

While this works fine I know it's far from perfect as it's limited to one search term. My thought was to add in another loop within pluck() but I'd prefer to avoid doing that, my second thought was to map the object and perform the lookup once I'd found an object with the first property match. Again any suggestions on how I might accomplish this whilst keeping the script clean and fast I'd really appreciate it.

Any other suggestions welcome, thank you for taking a look. :)

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

To answer your specific concern about jQuery-dependence, the jQuery extend function is basically this:

var key;
for(key in objB) {
  objA[key] = objB[key]; // copy each value from objB into objA
}

That's all there is to it really.

However, I see a number of other things in your code that I think deserve attention.

1. Why define pluck insides the AOHandler constructor, and everything else in its prototype? Just wondering, since it's an unnecessary mixing of style. pluck can just as easily go in the prototype.

2. You're using typeof ao === 'object' when determining whether to use the argument passed to the constructor or not. Use ao instanceof Array instead. Almost everything in JS is "typeof object", but what you're looking for is an array and only an array. (I'd probably be strict about it too and throw an exception if a non-array argument is passed).

3. Your code doesn't seem to handle multiple identical values very well. Say I do this:

var ao = new AOHandler(),
    obj = { foo: "bar" },
    dup = $.extend({}, obj);

ao.add(obj);
ao.add(dup);
ao.add(obj);

Now there are three items in the AOHandler's list, two of which are the exact same object, and the third being a copy. None of you functions seem prepared to deal with such a situation. For instance, calling update would update the first item, which happens to also be the third item, so in fact 2 items get updated. However, the 2nd item (which has the same values, but isn't the same object) would be untouched. Could get pretty confusing.

4. Your function names seems a little off to me. pluck usually means to pick out values from each item in a list (i.e. a map-like function); not the items themselves, and certainly not only the first one. filter, select, find, search, or fetch would all be better names, provided the function actually returns all the matches. filter is indeed the name used for the native JS function (available in ECMA 5) that, well, filters an array and return the subset that matches a comparison function.

However, your pluck can also return the index of an item. You call it key, but since this.ao is a normal numerically-indexed array index is the word to use. Key implies it won't change, but indices of an array do change when other items are added or removed.
Anyway, the usual JS name for a function that finds the index of a certain thing in an array is indexOf. So I'd suggest that you break the pluck function into an indexOf function and a filter function, since those are separate things.

You could also make the case for push instead of add, since AOHandler is pretty much a wrapper around an array, and you push items to arrays. But add is plain English (and the same that Java uses for its ArrayList), so it's fine to use.

update is more problematic though, because it really does 2 (very) different things: Merge or replace. Merge will update the object in-place, so whereever else you have a reference to it, the updates will be visible. Replacing the object is a different story altogether. Again, I'd suggest 2 functions, since the effects are so very different, and the implementations have little in common.

Here's me take:

function AOHandler(ao) {
  // if anything's passed, it must be an array
  if(typeof ao !== "undefined" && !(ao instanceof Array)) {
    throw new TypeError;
  }
  this.ao = ao || [];
}

AOHandler.prototype.indexOf = function (predicate, offset) {
  var i, l = this.ao.length;
  if(typeof predicate !== 'object') { throw new TypeError; } // type check
  offset = parseInt(offset, 10) || 0;
  // match native indexOf's handling of negative offset
  // see: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
  offset = (offset < 0 ? l - offset : offset);
  offset = offset < 0 ? 0 : offset;
  for( i = offset ; i < l ; i++ ) {
    if(this.ao[i][predicate.key] === this.ao[i][predicate.value]) {
      return i;
    }
  }
  return -1;
};

AOHandler.prototype.filter = function (predicate) {
  var offset = 0, index, results = [];
  while((index = this.indexOf(predicate, offset)) !== -1) {
    results.push(this.ao[index]);
    offset = index + 1;
  }
  return results;
};

// This, by the way, is a pluck function
AOHandler.prototype.pluck = function (key) {
  var i, l, results = [];
  for( i = 0, l = this.ao.length ; i < l ; i++ ) {
    results.push(this.ao[i][key]);
  }
  return results;
};

AOHandler.prototype.add = function (obj) {
  if(typeof obj !== 'object') { throw new TypeError; } // type check
  return this.ao.push(obj);
};

AOHandler.prototype.get = function (predicate) {
  var index = this.indexof(predicate);
   // return null - not false - if no obj is found
  return (index === -1 ? null : this.ao[index]);
};

// Simple jQuery-less merge function
AOHandler.prototype.update = function (predicate, source) {
  var matches = this.filter(predicate), i, l, key;
  if(typeof source !== 'object') { throw new TypeError; } // type check
  for( i = 0, l = matches.length ; i < l ; i++ ) {
    for(key in source) {
      // it may be a good idea to do a source.hasOwnProperty[key] check here
      matches[i][key] = source[key];
    }
  }
};

// replace all matches
AOHandler.prototype.replace = function (predicate, replacement) {
  var matches = this.filter(predicate), index, l, offset, key;
  if(typeof replacement !== 'object') { throw new TypeError; } // type check
  while((index = this.indexOf(predicate, offset)) !== -1) {
    this.ao[index] = replacement;
    offset = index + 1;
  }
};
\$\endgroup\$
  • \$\begingroup\$ Thank you for such a detailed answer! Ok I've seen the light in terms of making this far more extensible than I had it. I've followed your outline and now having it working far better! I'm unsure what the use if any of the pluck function is? You just give it a property name and it returns all values that have the property name across all objects. What's the use of that? Either way many many thanks for the guidance! :) \$\endgroup\$ – David Barker Nov 4 '12 at 10:39
  • \$\begingroup\$ @DavidBarker You're welcome. I included the pluck function just to illustrate; I can't say it's necessarily useful in your particular case. However, like any map-like function it can be very useful. For instance if you have an array of, say, book chapters. Each has a title and some content, and you want to make a table of contents, so you do chapters.pluck('title') (pseudo code) to get list of chapter titles. Simple example, but hopefully one that makes sense. Again, it's just a specialized map function \$\endgroup\$ – Flambino Nov 4 '12 at 13:57
  • \$\begingroup\$ Yes that does make a lot of sense, thanks again! \$\endgroup\$ – David Barker Nov 4 '12 at 14:35

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.