Alex Elyasov
Alex Elyasov

Reputation: 87

Intercepting JavaScript array accessor

I would like to associate some side effect with every array accessor like a[i]. For example, if the side effect is writing a message to the console, the following program:

var array = [1, 2, 3]
var total = 0;
for (var i in array) {
  total += array[i]
}
console.log(total);
should return the output like:

1 // access a[0]
2 // access a[1]
3 // access a[2]
6 // print original total

In case I was interested in intercepting an array method push, I'd use the technique from this blog post and provided an interceptor:

var _push = Array.prototype.push;
Array.prototype.push = function( item ) {
    console.log( 'pushing ', item, ' into ', this );
    _push.apply( this, arguments );
}

Is it possible apply the same trick to an array accessor? Or what would be a better solution for this problem? One important note is that I don't want to modify the original code of the program. Therefore, using JS proxies to intercept getters and setters doesn't seem te be a valid option for my problem.

One particular side effect, I want to introduce is raising an exception in case the accessed value is undefined (some variation of the index out of bounds exception for JS arrays.) I would check if the currently accessing value is equal to undefined, and throw an exception, in that case, otherwise just return the original value.

Upvotes: 5

Views: 3024

Answers (2)

le_m
le_m

Reputation: 20228

It is not possible without modifying the code:

So you need to either modify the code, preprocess your code or modify the JavaScript engine running the code.

In the first two cases, I recommend replacing array literals with explicit calls to the Array constructor, which can be overriden:

// Override default array constructor:
Array = (function(Array) {
  function LoggingArray(...args) {
    return new Proxy(Array(...args), {
      get: function(target, property) {
        console.log(target[property]);
        return Reflect.get(target, property);
      }
    });
  }
  Object.setPrototypeOf(LoggingArray, Array);
  LoggingArray.prototype = Array.prototype;
  return LoggingArray;
})(Array);

// Original code without array literal:
var array = Array(1, 2, 3);
var total = 0;
for (var i in array) {
  total += array[i]
}
console.log(total);

Upvotes: 2

quirimmo
quirimmo

Reputation: 9988

You can't override that accessor of Arrays. Here an example:

Object.defineProperty(Array.prototype, 0, {
  get: function () { return "my get on 0"; }
});
var a = [1,2,3];
console.log(a[0]); // output: 1

But if you try to do the same with a property which doesn't really exist in the array, you will achieve it:

Object.defineProperty(Array.prototype, 5, {
  get: function () { return "my get on 5"; }
});
var a = [1,2,3];
console.log(a[5]); // output: my get on 5

What you can do is a little workaround accessing the elements through the get method of Arrays.

Array.prototype.get = function(i) { 
  console.log('my print'); 
  console.log(this[i]); 
  return "this is!"; 
};
var a = [1,2,3];
console.log(a.get(0)); // output: my print 1 this is!

So, coming back to your question you could do something like you did for push but with the get, avoiding proxies:

Array.prototype.get = function (i) {
  console.log('Accessing element: ' + this[i]);
  console.log(this);
  return this[i];
};
var array = [1, 2, 3];
var total = 0;
// be careful that now you cannot do anymore 
// for (var i in array), because inside the array there is also the property get defined and it will cycle also on that
// if you want to cycle again in that way, you need the check through hasOwnProperty method
/*
for(var i in array) {
  if (array.hasOwnProperty(i)){
    console.log(i);
    total += array.get(i);
  } 
}
*/
for(var i = 0; i < array.length; i++) {
  total += array.get(i);
}
console.log(total);

Just for completing the answer, what you are trying to do can be done in one line with reduce method of Arrays:

var array = [1, 2, 3];
var result = array.reduce(function (accumulator, actual) {
  return accumulator + actual;
}, 0);
console.log(result);

I strongly recommend you to avoid the override of these accessors. You will change the basis of the code so it will be impossible for third party people to understand what's going on without reading all the code. Moreover you will lose a lot of built-in useful methods. I hope this helps

p.s. following to your edit, for checking undefined values and raise exceptions you can add the check inside the override of the get method. But my suggestion is just to filter the array, detect the undefined values and get rid of them. Note that I am using the double equal. because undefined == null but undefined !== null. In this way you will remove both undefined and null values. If you want to remove only undefined, change it to if (typeof element === 'undefined').

So something like this, using just one loop with arrays filter method:

var data = [1, 2, undefined, 3, 4, undefined, 5];

data = data.filter(function( element, index ) {
   // note that I am using the double equal. because undefined == null but undefined !== null. 
   // in this way you will remove both undefined and null values
   // if you want to remove only undefined, change it to if (typeof element === 'undefined')
   if (element == null) {
     console.log('found and undefined null value at index: ' + index);
   }
   return element != null;
});
console.log(data); // array without undefined and null values

Upvotes: 4

Related Questions