5

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.

Alex Elyasov
  • 87
  • 1
  • 9

2 Answers2

4

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
quirimmo
  • 9,800
  • 3
  • 30
  • 45
  • thanks for the reply. if what I want to achieve is not feasible, I'll have to fall back on the option with the `get` method suggested by you. I also updated the question a bit, clarifying the kind of exception I care most. – Alex Elyasov May 21 '17 at 15:46
  • you can change the get method in order to check if it is undefined before and raise the exception, but my suggestion is another one in this case. I will add it to the answer, it is easier to write code than in comments – quirimmo May 21 '17 at 15:58
  • changed it adding at the end your specific case – quirimmo May 21 '17 at 16:03
  • I didn't know the difference between `==` and `===` for `undefined`. It was useful! I don't actually want to remove anything from the array, just track if it's accessed out of bounds. By default, JS doesn't give any exception in that scenario, but I want to know when it happens. – Alex Elyasov May 21 '17 at 16:18
  • Maybe I got what it is happening to you :) you are working on an app wrote by someone else, quite big, they defined a lot of arrays but they didnt manage undefined cases, so you are having a lot of errors everywhere. So now you want a way to change directly the prototype so you dont have to refactor all the code. Isn'it? :D – quirimmo May 21 '17 at 16:22
  • "I strongly recommend you to avoid the override of these accessors." I agree. You could create a constructor which just delegates to `Array` (via prototypes), but override the `get` method. – David Knipe May 21 '17 at 16:31
  • @DavidKnipe yep agree, I am trying to understand now what's his situation. At the beginning the question was about override accessors without using proxies – quirimmo May 21 '17 at 16:33
  • No, you didn't @quirimmo I'm generating test data randomly for a JS function. I know the types of input parameters but not supposed to analyse the semantic relationship between them. For the function under test `foo(array, index)`, I may generate the following input `array = [1,2,3]` and `index = 5`. If somewhere in the body of `foo` I access `array[index]`, JS just returns `undefined`, no exception is thrown. Given JS behaviour doesn't help me to figure out my test machinery that the generated input is malformatted and should be discarded. – Alex Elyasov May 21 '17 at 16:34
2

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);
Community
  • 1
  • 1
le_m
  • 19,302
  • 9
  • 64
  • 74