5

This may seem silly, but in this day and age, one should be able to expect JS to raise an event if contents of an array have changed.

A few questions been asked regarding getting notified when a variable changes (define getter or setter). And there seems to be a way to do that (at least for most browsers including IE6+)

My issue is that I'm trying to get notified if an item inside an array changes:

    var ar = ["one", "two", "three"];
    // setting the whole array will call the custom setter method
    // (assuming you defined it)

    ar = ["one", "three", "five"];

    // however, this will only call the getter method and won't call the setter
    // without defining custom setters for every item in the array.  

    ar[1] = "two";

Obviously, I'm trying to avoid forcing the coder to use old-school Java style .getVale() and .setValue() functions to access/modify data.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
MandoMando
  • 5,215
  • 4
  • 28
  • 35
  • Backbone.js can trigger events when collection models change. Not quite the same thing, but you could see how they do it. – asawyer Jun 06 '12 at 15:42
  • 1
    You could have the array as a private variable of an object, then the only way to change the variable is use a method on that object, which can fire an event... – Abe Petrillo Jun 06 '12 at 15:45
  • 1
    I think you should read about the [observable pattern](http://jsclass.jcoglan.com/observable.html) – MilkyWayJoe Jun 06 '12 at 15:47
  • 2
    Possible duplicate of http://stackoverflow.com/questions/2449182/getter-setter-on-javascript-array Looks like you *can* use defineGetter/defineSetter on an array but only for fixed keys. `You can't define a property for “all names that happen to be integers” all in one go.` – Dmitry Pashkevich Jun 06 '12 at 15:47
  • @asawyer, Abe and MilkyWayJoe: great suggestions, but they all require mod to existing syntax array[indexz] = x; Dmitry: don't think duplicate, but a great post by bobince. This means that JS would need a onProperyAdded hook to then add a custom getter setter to the array items dynamically. If one of you would like to post that as the answer, think we're there. – MandoMando Jun 06 '12 at 16:05
  • @DmitryPashkevich Instead of using the deprecated `defineGetter/Setter` modern browsers all support and recommend using [`Object.defineProperty`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/defineProperty). – Phrogz Jun 06 '12 at 17:07

3 Answers3

3

In short: no, you can't. You'll notice that Arrays don't provide any event dispatching mechanism, and their API doesn't include any callback type functionality.

In longer: as others have noted, it is possible to wrap the array… And it's also possible to poll the arrays contents:

function watchArray(arr, callback) {
    var oldVal = "" + arr;
    setInterval(function() {
        var curVal = "" + arr;
        if (curVal != oldVal) {
            callback();
            oldVal = curVal;
        }
    }, 100);
}

But this method has some obvious problems: it polls, it'll get slow to watch a bunch of arrays, etc.

David Wolever
  • 148,955
  • 89
  • 346
  • 502
  • Thanks for this post. Since the getter does get called when an array element is changed, and yours is a way to compare => getter + delay + comapre = change notification. The slow issue is finding which element changed (less of an issue for me). – MandoMando Jun 06 '12 at 17:19
  • 1
    What about the new defineProperty method as suggested by @Phrogz above? – Dmitry Pashkevich Jun 06 '12 at 19:31
1

I think timeout-based solutions are not the best.
If you can only use push and pop to modify your array, you can override push and pop methods of Array prototype (or only some object that you want to monitor):

var myWatchableArr = [];
myWatchableArr.setChangeCallback = function(callback){
    this.changeCallback = callback;
}
myWatchableArr.push = function(e){
    Array.prototype.push.call(this,e);
    if(typeof this.changeCallback == "function")
      this.changeCallback(this);
}
myWatchableArr.push(3);
myWatchableArr.setChangeCallback(function(arr){
    console.log("the array has been changed", arr);
});
// now watching for changes
myWatchableArr.push(4);

If push and pop are not sufficient, you can add some setAt method to use like myWatchableArr.setAt(3, value) instead of myWatchableArr[3]=value.

Alireza Mirian
  • 5,862
  • 3
  • 29
  • 48
0

Ok, based on @David Wolever's code and other comments, there actually is a solution:

Use the notes from John Dyer to implement the addProperty method. Place a setTimeout in the getter method to compare with original value a short time after the read takes place:

addProperty(myObject, 'vals',
    function () {
        var _oldVal = "" + this._val;
        var _parent = this;
        console.log('getter!');
        setTimeout(function () {
            var curVal = "" + _parent._val;
            if (curVal != _oldVal)
                console.log('array changed!');
        }, 200);
        return this._val;
    },
    function (value) {
        console.log('setter!');
        this._val = value;
    });

    myObject.vals = ["one", "two", "three"];
    myObject.vals[1] = "five";
MandoMando
  • 5,215
  • 4
  • 28
  • 35