5

I haven't found any information on this topic so forgive me please if this is a very weird question.

I know JS allows to define properties as accessors meaning they fire a getter or a setter function when used.

My question is whether the same can be done to array members.

For example, I want a setter function to fire when assigning like this:

myObj[2] = 2 /* set function(value, index) { console.log(value + index) } */

If this is not possible, are there any other ways one can extend the [] operation?

4 Answers4

6

Basically unless you subclass an array you can not.

Even if you subclass, arrays are much more dynamic than objects. Unlike objects they get items added and removed all the time and you have to be able to dynamically attach getters and setters to each property (key) along with the added item. Not practical. If an array is sub-classed you may modify it's length property with getters and setters and try to watch the array from the changes to it's length property but this requires a lot of array diffing.

I guess with the arrays it's best to use ES6 Proxy object. You might do it as follows;

function ProxyMaker(a){
    return new Proxy(a, {
        get: function(target, property, receiver) {
            console.log(target+"'s "+property+" property has been accessed");
            return target[property];
        },
        set: function(target, property, value, receiver) {
            console.log(target+"'s "+property+" property has been modified");
            return target[property] = value;
        },
        has: function(target, property) {
            console.log(target+"'s "+property+" property has been checked");
            return Reflect.has(target, property); // 10x to @Bergi
        }
    });
}

var p = ProxyMaker([]);
p[0] = 1;
p[0];
p.push(10);
p.unshift(7);
console.log(JSON.stringify(p))
Alexis Tyler
  • 1,394
  • 6
  • 30
  • 48
Redu
  • 25,060
  • 6
  • 56
  • 76
  • @Bergi yes :) what was the right way with Reflect..? – Redu Oct 13 '16 at 20:21
  • 1
    Probably `Reflect.has(target, property)`, afaik they were designed to have the exact same signature as the trap so it's easy even without remembering details – Bergi Oct 13 '16 at 20:22
1

If it is critical to know when a value has changed you would need to create explicit setters and getters.

As suggested Proxy is a good option if available. It may not be available to in all browsers though. In this case you could create a new object that notifies you when it was set.

There a lot of trade offs with this approach, and it would be a fair amount of work to implement everything array natively provides. The example below is pretty bare-bones but it does provide array like interactions and a setter event to listen to.

var observableArray = function(eventName) {
    var myArray = [];

    var api = {};

    api.get = function(index) {
        if (myArray.length && typeof index === 'number') {
            return myArray[index];
        }
    };

    api.set = function(item, index) {
        if (typeof index === 'number') {
            myArray[index] = item;
        }
        else {
            index = myArray.push(item) - 1;

        }
        var event = new CustomEvent(eventName, {detail: index});
        document.dispatchEvent(event);
        return index;
     };
    api.getLength = function() {
        return myArray.length;
    };

    api.empty = function() {
        myArray.splice(0, myArray.length);
    };

    return api;
};

var arrayListener = document.addEventListener('foo', function(e)     {console.log('The index modified: ',e.detail);},false);

var test = new observableArray('foo');
test.set('bar');
console.log('The # of items: ', test.getLength());
test.empty();
console.log('The # of items after empty: ', test.getLength());

If you only needed a way to be able to make something happen when an array value is set, you could extend the array to have a set method that fires a callback. It would be up to the programmer to use it though.

    Array.prototype.set = function(item, index, callback) {
    if (typeof index === 'number') {
        this[index] = item;
    }
    else {
        index = this.push(item) - 1;

    }
    if (callback) {
        callback.apply(this, [index]);
    }
    return index;
};

var test2 = [];
test2.set('foo', 4, function(i) {
    console.log('Callback value: ', i);
    console.log('The array: ', this);
});
Ron Sims II
  • 566
  • 5
  • 10
  • I don't know which one to give the answer tag to... Your answer doesn't use ES6 but at the same time it doesn't extend the `[/*Number*/]` operator like accessors extend the `=` operator. – user1533299 Oct 14 '16 at 13:50
0

What you can do is wrapping the object in an ES6 proxy.

var a = new Proxy({}, {get:function(target, name){return 42;}});

console.log(a[0]); // ==> 42

then you can trap and add processing to get/set operations.

6502
  • 112,025
  • 15
  • 165
  • 265
0

You have two ways of accessing arrays:

  1. treated like an object with a named setter/getter var x = pojoArray['firstName'];

  2. through the numerical index var x = pojoArray[10];

So you have two ways of approaching this as I see it:

Bad Way: If you treat the array like an object, you could loop the entire array manually but that should be avoided while using arrays like objects because order is not guaranteed. See the following post: Loop through an array in JavaScript

Other Option: Per your requirement, the other way to do this is to access it through the numerical index. You could create some sort of helper functions that will have to map a getter and setter key with the proper index in the array and ensure that that key is only used once.

Notice that I did not state a 'good way' to fulfill your requirement. I would try and avoid either of these workflows if at all possible, as they will be prone to errors and cause headache down the road.

Community
  • 1
  • 1
mokuteki
  • 21
  • 3