3

There is so much about creating and inheritance of Array-like objects, but one of my questions remains unanswered: is it possible somehow to add a auto-incrementing .length property to the object?
Sample script:

function ArrayLike(){
    this.length=0;
    this.splice=[].splice;
}
var Arr=new ArrayLike();
Arr[0]=0;
Arr[1]=1;
alert(Arr.length); //alerts "0" instead of "2"

Of course, i can add some method like:

function ArrayLike(){
    this.length=0;
    this.splice=[].splice;
    this.push=function(toadd){
        this[this.length++]=toadd;
    }
}

But this is not what I'm trying to do.

So is it possible to increment .length when I add something to the object in a way Arr[N]='blah-blah-blah'?

Cerbrus
  • 70,800
  • 18
  • 132
  • 147
MadBrozzeR
  • 341
  • 3
  • 15
  • 1
    Why would you want to do this? Just use an `Array()`, and add extra properties to it, if you with. In JavaScript, `Array`s are also objects. They _can_ have properties: `var Arr = [0, 1]; arr.myProperty = "foobar";` I'm not saying any of this is a good idea, though. – Cerbrus Jan 29 '15 at 09:10
  • Your best option is probably making length a method, not a property. – Etheryte Jan 29 '15 at 09:10
  • Please don't downvote this question. It is not written badly and does not fail in any other metric. If he fails to see why this is not a good idea, then the answer should explain why, that's all. – Neil Jan 29 '15 at 09:19
  • @Cerbrus The main aim is to clone Array object without unnecessary (in my case) methods (join, concat, forEach, etc.), but extended with methods I need. To win a small bit of performance while iterating, or just not to have it overloaded with extra methods. Not that I really in a need of using `Arr[N]='value'` method, but it would be really nice to have. – MadBrozzeR Jan 29 '15 at 10:17
  • 1
    @MadBrozzeR: You really won't gain _any_ performance by copying part of the `Array` functionality. It's a native object that's well-optimized in modern browsers. Writing your own implementation will negate that optimization, not to mention the loops you'd have to jump through to actually make it work. – Cerbrus Jan 29 '15 at 10:22
  • 1
    @MadBrozzeR: *"To win a small bit of performance while iterating..."* I can't see any gain by using your own object over a native array, can you explain where you think you'll get that gain? *"...or just not to have it overloaded with extra methods."* The "extra" methods don't "load" individual array instances in any way at all. Individual array instances have only their own properties, the "extra" methods are on the *prototype* that sits behind them. There is zero cost associated with them on a per-object basis. – T.J. Crowder Jan 29 '15 at 10:40
  • I guess you are right. So, let's say, this was an educational case. – MadBrozzeR Jan 29 '15 at 12:31
  • Then will it be better performance solution to inherit Array object and to extend it with desired methods? – MadBrozzeR Jan 29 '15 at 12:37
  • @MadBrozzeR: Sadly, even with ES5 you can't quite inherit from `Array`, some of its magic is too magic. Simplest thing is to "mix in" your methods on specific array instances. Or if you like, you can add your methods to `Array.prototype`. In both cases, just be careful to make the new properties for those non-enumerable, or make sure none of your code is using `for-in` for looping arrays without safeguards (more about that [in this answer](http://stackoverflow.com/questions/9329446/for-each-over-an-array-in-javascript/9329476#9329476)). – T.J. Crowder Jan 29 '15 at 12:50
  • @T.J.Crowder "_Simplest thing is to "mix in" your methods on specific array instances._" - Just the way I'm doing it right now... searched for better solution. `"Array.prototype"` - I try to avoid native prototypes overloading. – MadBrozzeR Jan 29 '15 at 15:14
  • @MadBrozzeR: No, the opposite of what you're doing. I'll demonstrate in my answer in a mo'.... – T.J. Crowder Jan 29 '15 at 15:27

1 Answers1

3

is it possible somehow to add autoincrement .length property

Not really; at least, you can't do all the things Array does around length and index properties.

In an ES5-enabled environment, you can make length a property with getter and setter functions. But it's awkward to find out what length should be, because there's (so far) no "catch all" way of setting it when code like:

arr[1] = "foo";

...runs. (ES6 may well make it possible to have catch-all property setters, via proxies.)

So your length would have to figure it out from the object contents, which is not going to be very efficient, e.g.:

var explicitLength;
Object.defineProperty(this, "length", {
    get: function() {
        var length;

        // Find our length from our max index property
        length = Object.keys(this).reduce(function(maxIndex, key) {
            var index = key === "" ? NaN : +key;
            if (!isNaN(index) && index > maxIndex) {
                return index;
            }
            return maxIndex;
        }, -1) + 1;

        // If we have an explicitly-set length, and it's greater,
        // use that instead. Note that if explicitLength is
        // `undefined`, the condition will always be false.
        if (explicitLength > length) {
            length = explicitLength;
        }

        return length;
    },
    set: function(value) {
        explicitLength = value;
        // You might choose to have code here removing properties
        // defining indexes >= value, likes Array does
    }
});

Note that even when we have an explicitly-set length, we still have to check to see if the natural length is greater, to deal with:

arr.length = 2;
arr[5] = "foo";
console.log(arr.length); // Should be 6, not 2

Doing that much work on a property retrieval is obviously not a good thing, so I would steer clear of this sort of thing.


Above I said "...so I would steer clear of this sort of thing."

Well that's all very well and good, but what should you do instead?

Suppose you have the functions nifty and spiffy that you want to have available. You have two realistic options:

  1. Use an actual array, and mix in your custom methods to each array instance

  2. Enhance Array.prototype

#1 Use an actual array and mix in your methods

You'd define your methods on a convenient object, say ArrayMixin:

var ArrayMixin = {
    nifty: function() {
        // ...do something nifty with `this`
    },
    spiffy: function() {
        // ...do something spiffy with `this`
    }
};

Then have a builder function for creating arrays:

function createArray(arr) {
    arr = arr || []; // `arr` is optional
    extend(arr, ArrayMixin);
    return arr;
}

What's this extend function, you ask? Something that copies properties from one object to another. If you use any libraries or frameworks (jQuery, Underscore, Angular, PrototypeJS, ...), they're likely to have an extend function of some kind that does this. The usually look something like this (this is very off-the-cuff).

function extend(target) {
    var arg, obj, key;
    for (arg = 1; arg < arguments.length; ++arg) {
        obj = arguments[arg];
        for (key in obj) {
            if (obj.hasOwnProperty(key)) {
                target[key] = obj[key];
            }
        }
    }
    return target;
}

Note that that's a shallow copy, and it doesn't bother to try to make the properties it adds to target non-enumerable. Tweak as desired.

So then when you want an array:

var a = createArray(['one', 'two', 'three']);

...and a has nifty and spiffy on it.

Example:

var ArrayMixin = {
  nifty: function() {
    this.forEach(function(entry, index) {
      this[index] = entry.toUpperCase();
    }, this);
    return this;
  },
  spiffy: function() {
    this.forEach(function(entry, index) {
      this[index] = entry.toLowerCase();
    }, this);
    return this;
  }
};

function createArray(arr) {
  arr = arr || []; // `arr` is optional
  extend(arr, ArrayMixin);
  return arr;
}

function extend(target) {
  var arg, obj, key;
  for (arg = 1; arg < arguments.length; ++arg) {
    obj = arguments[arg];
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        target[key] = obj[key];
      }
    }
  }
  return target;
}

var a = createArray(['one', 'two', 'three']);
a.nifty();
snippet.log(a.join(", "));
a.spiffy();
snippet.log(a.join(", "));
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>

Enhance Array.prototype

People freak out about enhancing Array.prototype, but there's really nothing wrong with it. The only reason it causes trouble is if people use broken code that expects for-in to loop through array indexes and you've had to enhance the prototype without making the new methods non-enumerable. But again, that code is broken; for-in isn't for looping through array indexes, it's for looping through object properties.

So here's enhancing Array.prototype:

(function(aproto) {
    var add;
    if (Object.defineProperty) {
        // Function to add a non-enumerable property
        add = function(obj, prop, value) {
            Object.definePrperty(obj, prop, {
                value: value
            });
        };
    } else {
        // Old JavaScript engine, oh well
        add = function(obj, prop, value) {
            obj[prop] = value;
        };
    }

    add(aproto, "nifty", function() {
        // ...do something nifty with `this`
    });

    add(aproto, "spiffy", function() {
        // ...do something spiffy with `this`
    });

})(Array.prototype);

Now, using it is really clean:

var a = ['one', 'two', 'three'];

...and a has nifty and spiffy on it.

Example:

(function(aproto) {
  var add;
  if (Object.defineProperty) {
    // Function to add a non-enumerable property
    add = function(obj, prop, value) {
      Object.defineProperty(obj, prop, {
        value: value
      });
    };
  } else {
    // Old JavaScript engine, oh well
    add = function(obj, prop, value) {
      obj[prop] = value;
    };
  }

  add(aproto, "nifty", function() {
    this.forEach(function(entry, index) {
      this[index] = entry.toUpperCase();
    }, this);
  });

  add(aproto, "spiffy", function() {
    this.forEach(function(entry, index) {
      this[index] = entry.toLowerCase();
    }, this);
  });

})(Array.prototype);


var a = ['one', 'two', 'three'];
a.nifty();
snippet.log(a.join(", "));
a.spiffy();
snippet.log(a.join(", "));
<!-- Script provides the `snippet` object, see http://meta.stackexchange.com/a/242144/134069 -->
<script src="http://tjcrowder.github.io/simple-snippets-console/snippet.js"></script>
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • ^ This is why it's basically a good idea to work with a `Array()` as your "source". Extend that (if you _have_ to), instead of trying to copy functions. – Cerbrus Jan 29 '15 at 09:23
  • Well, this is totally not a way to simplicity, but anyway, the answer satisfies the question. – MadBrozzeR Jan 29 '15 at 12:43
  • @MadBrozzeR: Re simplicity: Exactly. :-) Much better off just using an array. – T.J. Crowder Jan 29 '15 at 12:48
  • @T.J.Crowder Like I said before, the way I do it right now (in my js library, not here) is very similar to _"Use an actual array, and mix in your custom methods to each array instance"_, but without an extra object: `var arr=NLtoARR(NodeList);` `arr.Nprops=1;` `arr.Nmeths=function(){return};` `return arr;` I came here to ask for better solution. So, I guess, I prefer this one now. Thank you. The answer is accepted. – MadBrozzeR Jan 31 '15 at 10:29
  • Just checked for performance between `new Array()` and `new ArrayLike()` (custom object). The first one is much faster. – MadBrozzeR Jan 31 '15 at 10:37
  • @MadBrozzeR: Right. And you basically never need to use `new Array()`. Use `[]` instead. Faster still. – T.J. Crowder Jan 31 '15 at 10:47