36

Is there a way to get a get/set behaviour on an array? I imagine something like this:

var arr = ['one', 'two', 'three'];
var _arr = new Array();

for (var i = 0; i < arr.length; i++) {
    arr[i].__defineGetter__('value',
        function (index) {
            //Do something
            return _arr[index];
        });
    arr[i].__defineSetter__('value',
        function (index, val) {
            //Do something
            _arr[index] = val;
        });
}
Adam
  • 4,007
  • 1
  • 23
  • 54
Martin Hansen
  • 4,774
  • 3
  • 28
  • 47

10 Answers10

49

Using Proxies, you can get the desired behavior:

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

var accessCount = 0;
function doSomething() {
  accessCount++;
}

var arr = new Proxy(_arr, {
  get: function(target, name) {
    doSomething();
    return target[name];
  }
});

function print(value) {
  document.querySelector('pre').textContent += value + '\n';
}

print(accessCount);      // 0
print(arr[0]);           // 'one'
print(arr[1]);           // 'two'
print(accessCount);      // 2
print(arr.length);       // 3
print(accessCount);      // 3
print(arr.constructor);  // 'function Array() { [native code] }'
<pre></pre>

The Proxy constructor will create an object wrapping our Array and use functions called traps to override basic behaviors. The get function will be called for any property lookup, and doSomething() before returning the value.

Proxies are an ES6 feature and are not supported in IE11 or lower. See browser compatibility list.

aebabis
  • 3,392
  • 3
  • 18
  • 39
19

Array access is no different to normal property access. array[0] means array['0'], so you can define a property with name '0' and intercept access to the first array item through that.

However, that does make it impractical for all but short, more-or-less-fixed-length Arrays. You can't define a property for “all names that happen to be integers” all in one go.

bobince
  • 498,320
  • 101
  • 621
  • 807
  • 13
    Interesting strategy. I'm still waiting for JavaScript to introduce a catch-all getter/setter mechanism and I will be a happy duck! – devios1 Feb 07 '12 at 16:28
  • Worth mentioning that being an Array key is defined in the specification as being able to convert they keys to a unsigned 32 bit integer. Nice answer. – Benjamin Gruenbaum Jun 07 '13 at 18:55
  • I implemented @bobince's suggestion [here](http://michaelgeekbrewer.blogspot.com/2013/09/capturing-array-element-edits.html). It works well for everything except if the array is accessed outside the established array limits. – Constablebrew Oct 10 '13 at 16:03
  • it has been 4 years.. is it possible yet? i'm wracking my brain on this one as i really need this feature for my web app.. – Michael Jun 28 '14 at 02:16
  • 13
    @devios It exists now: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy – aebabis Mar 10 '15 at 22:19
5

I looked up in John Resig's article JavaScript Getters And Setters, but his prototype example didn't work for me. After trying out some alternatives, I found one that seemed to work. You can use Array.prototype.__defineGetter__ in the following way:

Array.prototype.__defineGetter__("sum", function sum(){
var r = 0, a = this, i = a.length - 1;
do {
    r += a[i];
    i -= 1;
} while (i >= 0);
return r;
});
var asdf = [1, 2, 3, 4];
asdf.sum; //returns 10

Worked for me in Chrome and Firefox.

rolandog
  • 139
  • 2
  • 7
1

It is possible to define Getters and Setters for JavaScript arrays. But you can not have accessors and values at the same time. See the Mozilla documentation:

It is not possible to simultaneously have a getter bound to a property and have that property actually hold a value

So if you define accessors for an array you need to have a second array for the actual value. The following example illustrates it.

//
// Poor man's prepare for querySelector.
//
// Example:
//   var query = prepare ('#modeler table[data-id=?] tr[data-id=?]');
//   query[0] = entity;
//   query[1] = attribute;
//   var src = document.querySelector(query);
//
var prepare;
{
  let r = /^([^?]+)\?(.+)$/; // Regular expression to split the query

  prepare = function (query, base)
  {
    if (!base) base = document;
    var q  = []; // List of query fragments
    var qi = 0;  // Query fragment index
    var v  = []; // List of values
    var vi = 0;  // Value index
    var a  = []; // Array containing setters and getters
    var m;       // Regular expression match
    while (query) {
      m = r.exec (query);
      if (m && m[2]) {
        q[qi++] = m[1];
        query   = m[2];
        (function (qi, vi) {
          Object.defineProperty (a, vi, {
            get: function() { return v[vi]; },
            set: function(val) { v[vi] = val; q[qi] = JSON.stringify(val); }});
        })(qi++, vi++);
      } else {
        q[qi++] = query;
        query   = null;
      }
    }
    a.toString = function () { return q.join(''); }
    return a;
  }
}

The code uses three arrays:

  1. one for the actual values,
  2. one for the JSON encoded values
  3. and one for the accessors.

The array with the accessors is returned to the caller. When a set is called by assigning a value to the array element, the arrays containing the plain and encoded values are updated. When get gets called, it returns just the plain value. And toString returns the whole query containing the encoded values.

But as others have stated already: this makes only sense, when the size of the array is constant. You can modify the existing elements of the array but you can not add additional elements.

ceving
  • 16,775
  • 7
  • 82
  • 137
1

I hope it helps.

Object.extend(Array.prototype, {
    _each: function(iterator) {
                    for (var i = 0; i < this.length; i++)
                    iterator(this[i]);
                },
    clear: function() {
                    this.length = 0;
                    return this;
                },
    first: function() {
                    return this[0];
                },
    last: function() {
                return this[this.length - 1];
                },
    compact: function() {
        return this.select(function(value) {
                                                return value != undefined || value != null;
                                                }
                                            );
        },
    flatten: function() {
            return this.inject([], function(array, value) {
                    return array.concat(value.constructor == Array ?
                        value.flatten() : [value]);
                    }
            );
        },
    without: function() {
        var values = $A(arguments);
                return this.select(function(value) {
                        return !values.include(value);
                }
            );
    },
    indexOf: function(object) {
        for (var i = 0; i < this.length; i++)
        if (this[i] == object) return i;
        return -1;
    },
    reverse: function(inline) {
            return (inline !== false ? this : this.toArray())._reverse();
        },
    shift: function() {
        var result = this[0];
        for (var i = 0; i < this.length - 1; i++)
        this[i] = this[i + 1];
        this.length--;
        return result;
    },
    inspect: function() {
            return '[' + this.map(Object.inspect).join(', ') + ']';
        }
    }
);
Andy E
  • 311,406
  • 78
  • 462
  • 440
Martinez
  • 19
  • 1
  • 1
    This is actually a great idea, and should be properly explained. The code is creating a subclass of Array. The key thing is the extending `Array.prototype`. A more relevant answer would be `var SubArrayClass = {}; SubArrayClass.prototype = Object.extend(Array.prototype, { get: ..., set: ... });` – 00500005 Sep 29 '14 at 14:54
  • 2
    Well, a great idea that I wished worked. There's currently no way to intercept `Object[property]`, which is really whats needed to override. This just provides an Array like Object, which really isn't what the poster is asking for. – 00500005 Sep 29 '14 at 15:55
0

It is possible to create setters for each element of an array, but there is one limitation: you would not be able to directly set array elements for indexes that are outside the initialized region (e.g. myArray[2] = ... // wouldn't work if myArray.length < 2) Using the Array.prototype functions will work. (e.g. push, pop, splice, shift, unshift.) I give an example of how to accomplish this here.

Constablebrew
  • 736
  • 1
  • 6
  • 22
0

You can add whatever methods you like to an Array, by adding them to Array.prototype. Here's an example that adds a getter and setter

Array.prototype.get = function(index) {
  return this[index];
}

Array.prototype.set = function(index, value) {
  this[index] = value;
}
simonzack
  • 16,188
  • 11
  • 62
  • 100
Dónal
  • 176,670
  • 166
  • 541
  • 787
  • Sounds very functional and compatible, but If you wanna tweak an Array for good, than you don't want an Array anymore, but a class as @jpabluz said – Fabiano Soriani Mar 15 '10 at 18:01
  • 6
    But what I want is for the array to seemingly operate as before, so I can do arr[0] = "value" and not arr.set() etc.. and still execute some code when that is done. This is the way getter/setters function for normal properties. – Martin Hansen Mar 15 '10 at 18:56
0

Why not create a new class for the inner objects?

var a = new Car();

function Car()
{
   // here create the setters or getters necessary
}

And then,

arr = new Array[a, new Car()]

I think you get the idea.

jpabluz
  • 1,150
  • 6
  • 16
  • 1
    `new Array[ ... ]` is not syntactically correct. You have to use parens for instead: `new Array( ... )`. Or just leave out the `new Array` and just use the ` [ ... ]` literal notation. – Šime Vidas Oct 25 '10 at 16:20
0

this is the way I do things. You will have to tweak the Prototype Creation (I removed a bit from my Version). But this will give you the default getter / setter behavior I am used to in other Class-Based Languages. Defining a Getter and no Setter means that writing to the element will be ignored...

Hope this helps.

function Game () {
  var that = this;
  this._levels = [[1,2,3],[2,3,4],[4,5,6]];

  var self = {
    levels: [],
    get levels () {
        return that._levels;
    },
    setLevels: function(what) {
        that._levels = what;
        // do stuff here with
        // that._levels
    }
  };
  Object.freeze(self.levels);
  return self;
}

This gives me the expected behavior of:

var g = new Game()
g.levels
/// --> [[1,2,3],[2,3,4],[4,5,6]]
g.levels[0]
/// --> [1,2,3]

Taking up the critizism from dmvaldman: Writing should now be impossible. I rewrote the code to 1)not use depracated elements (__ defineGetter __) and 2) not accept any writing (that is: uncontrolled writing) to the levels element. An example setter is included. (I had to add spacing to __ defineGetter because of markdown)

From dmvaldmans request:

g.levels[0] = [2,3,4];
g.levels;
/// --> [[1,2,3],[2,3,4],[4,5,6]]

//using setter
g.setLevels([g.levels, g.levels, 1,2,3,[9]]);
g.levels;
/// --> [[[1,2,3],[2,3,4],[4,5,6]],[[1,2,3],[2,3,4],[4,5,6]], ....]

//using setLevels
g.setLevels([2,3,4]);
g.levels;
/// --> [2,3,4]
LeTigre
  • 400
  • 3
  • 13
  • g.levels[0] = [2,3,4] will not be intercepted by the setter. This is the functionality the original poster is asking for. – dmvaldman May 14 '15 at 01:04
  • @dmvaldman. Well the question askes about getters and setters generally. And your critizism would account for most of the other answers too, so... well nevermind. I reworked the code not to accept writing to the levels.Hope you like that better. Constructive critizism welcome. Thx.. – LeTigre Sep 11 '15 at 16:49
  • Again, no. There is a reason this reply is downvoted: it's misunderstanding the question. After g.levels[0] = [2,3,4] the poster is expecting [[2,3,4],[2,3,4],[4,5,6]] – dmvaldman Sep 12 '15 at 20:15
  • Sorry, but you do realize that that was exactly what it was doing when you downvoted it, right? Your comment was therefore: g.levels[0] = [2,3,4] will not be intercepted... and the result then was [[2,3,4],[2,3,4],[4,5,6]] after g.levels[0] = [2,3,4]. I changed that because you seemed to dislike that it did not intercept. So now you want it not to intercept. – LeTigre Sep 13 '15 at 19:01
  • I think a setter with a setLevels(what, where) syntax is not too far from what the poster is asking. – LeTigre Sep 13 '15 at 20:00
0

This answer is just an extension to the solution based on Proxy. See the solution with proxy, in that only get is mentioned but we can also use set as I am showing here.

Notice: 3rd argument in set can carry the value...

The code is self explanatory.

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

var accessCount = 0;

function doSomething() {
  accessCount++;
}

var arr = new Proxy(_arr, {
  get: function(target, name) {
    doSomething();
    return target[name];
  },
  set: function(target, name, val) { doSomething(); target[name] = val; }
});

function print(value) {
  document.querySelector('pre').textContent += value + '\n';
}

print(accessCount);      // 0
print(arr[0]);           // 'one'
print(accessCount);      // 1
arr[1] = 10;
print(accessCount);      // 2
print(arr[1]);           // 10
bittnkr
  • 89
  • 10
Tibin Thomas
  • 664
  • 4
  • 12