Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Which Objects in JavaScript have a .length property? (aka Why does Underscore _.each treat my Function Object like an Array?)

I've been under the impression that only Array objects have a .length property. But, then again, I've also seen mentions of objects that are "array-like". I've not looked into this, and now it seems like my ignorance of this topic in JS may be biting me in the ass. Case in point:

I've got the following code:

var View = function(options) {
  // code
};

_.extend(View, Backbone.Events, {

    make_children: function(parent) {
      // code
    }

});

Later on, I use this View Function with Underscore's _.each, which decides this function object is an array, because it has a .length property:

// Code from Underscore.js's `_.each`:
} else if (obj.length === +obj.length) { // This is true
  for (var i = 0, l = obj.length; i < l; i++) { // **So, execution goes here**
    if (iterator.call(context, obj[i], i, obj) === breaker) return
  }
} else {
  for (var key in obj) {
    if (_.has(obj, key)) { // **Execution does __not__ go here**
      if (iterator.call(context, obj[key], key, obj) === breaker) return;
    }
  }
}

This results in code that doesn't work, because obj[i] where i is an integer index, is not actually defined on my obj View. To be precise, in the above code, obj[0] is undefined while obj.length === +obj.length is true and obj.length is 1. What's going on here?

Addendum

Underscore's chief maintainer says the following on https://github.com/documentcloud/underscore/pull/510:

Simply making each reject function objects doesn't really help. We've made a conscious decision to use a numerical length property to detect array-like objects.

Instead, don't pass function objects to each.

Addendum 2

Realized that since I couldn't pass a function object to _.each, I could just "cast it" to a regular object like so:

var regular_obj = _.extend({}, View);
like image 797
Dmitry Minkovsky Avatar asked Dec 26 '22 07:12

Dmitry Minkovsky


2 Answers

The issue here is that underscore.js, much like jquery, both use the .length property as a flag in their each functions. When the length property is present, the function assumes that the argument passed can be iterated through with a normal for loop. The reason behind this logic is there is an expectation that when the length property is defined then it is possible to iterate through the argument in order which is why the for loop is used.

The result of misusing length is essentially a name collision where there is an unintended result. I would suggest changing length to another synonym such as size or capacity or totalViews, etc.


Edit

If there are no other alternatives for you to use, and you must have length in there while still retaining _.each's functionality, then you can slightly hack it. This plug works with the minified version of underscore version 1.4.3

var s = Array.prototype.ForEach;
var r = {};
var myEach = function (n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length&&typeof(n[0])!="undefined"){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(_.has(n,a)&&t.call(e,n[a],a,n)===r)return};
_.each=myEach;

Here is a demo: http://jsfiddle.net/Xa5qq/

Basically what it does is use forEach when the length property exists but typeof(yourObject[0]) == "undefined".

like image 124
Travis J Avatar answered Jan 18 '23 22:01

Travis J


Which Objects in JavaScript have a .length property?

By oh-so-tautological definition, any object which has a length property.

This happens to include functions.

length is a property of a function object, and indicates how many arguments the function expects, i.e. the number of formal parameters.

This is also array-like, because it has a length:

var foo = {
    bar: true,
    baz: 'quux',
    length: 42
}
like image 21
Matt Ball Avatar answered Jan 18 '23 23:01

Matt Ball