106

I can't seem to find the way to overload the [] operator in javascript. Anyone out there know?

I was thinking on the lines of ...

MyClass.operator.lookup(index)
{
    return myArray[index];
}

or am I not looking at the right things.

Neuron
  • 5,141
  • 5
  • 38
  • 59
slyprid
  • 1,179
  • 2
  • 9
  • 9

11 Answers11

106

You can do this with ES6 Proxy (available in all modern browsers)

var handler = {
    get: function(target, name) {
        return "Hello, " + name;
    }
};
var proxy = new Proxy({}, handler);

console.log(proxy.world); // output: Hello, world
console.log(proxy[123]); // output: Hello, 123

Check details on MDN.

Christoph Rackwitz
  • 11,317
  • 4
  • 27
  • 36
average Joe
  • 4,377
  • 2
  • 25
  • 23
  • 8
    How would we use this to create our own class with an index accessor? i.e. I want to use my own constructor, I don't want to construct a Proxy. – mpen Apr 21 '16 at 03:42
  • 3
    This isn't true overloading. Instead of calling methods on the object itself, you are now calling methods of the proxy. – Pacerier Nov 23 '17 at 06:33
  • @Pacerier you can return `target[name]` in the getter, OP is just showing the examples – Valen May 30 '19 at 21:27
  • 2
    Works with `[]` operator as well, btw: `var key = 'world';` `console.log(proxy[key]);` – Klesun Oct 13 '19 at 17:03
  • Though the question is with respect to overloading [] operator. This one is the perfect answer, as it serves the intended purpose very well as explained. – Pandurang Patil Jan 10 '22 at 11:24
  • Note that numbers are not special. If you check `typeof name` for in `proxy[123]` or `proxy[new Date()]` you'll see they are all [coerced to strings](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Property_Accessors#property_names). – Beni Cherniavsky-Paskin Jul 07 '22 at 10:42
  • Adding to what @BeniCherniavsky-Paskin said, this means that you can't get/set with anything that isn't a string/number/Symbol. I you wanted to pass a function or object as a prop, it won't work. – Magmagan Aug 18 '22 at 15:34
84

You can't overload operators in JavaScript.

It was proposed for ECMAScript 4 but rejected.

I don't think you'll see it anytime soon.

Peter Bailey
  • 105,256
  • 31
  • 182
  • 206
  • 6
    This may be doable with proxies in some browsers already, and will be coming to all browsers at some point. See https://github.com/DavidBruant/ProxyArray – Tristan Sep 19 '11 at 19:50
  • Then how does jQuery return different things dependent on whether you use [] or .eq()? http://stackoverflow.com/a/6993901/829305 – Rikki Oct 16 '15 at 20:45
  • 2
    You can do it now with a Proxy. – Eyal May 05 '17 at 19:21
  • although you can define methods with symbols in them as long as you access them as an array rather than with ".". That's how SmallTalk maps things like `Object arg1: a arg2: b arg3: c` as `Object["arg1:arg2:arg3:"](a,b,c)`. So you can have `myObject["[]"](1024)` :P – Dmytro Jul 24 '17 at 12:03
  • 3
    The link is dead :( – Gp2mv3 Jan 27 '18 at 14:21
  • All modern browsers have `Proxy` now. – T.J. Crowder Apr 16 '21 at 11:17
  • @Rikki - By just storing things at those indexes in the object. – T.J. Crowder Apr 16 '21 at 11:17
15

The simple answer is that JavaScript allows access to children of an Object via the square brackets.

So you could define your class:

MyClass = function(){
    // Set some defaults that belong to the class via dot syntax or array syntax.
    this.some_property = 'my value is a string';
    this['another_property'] = 'i am also a string';
    this[0] = 1;
};

You will then be able to access the members on any instances of your class with either syntax.

foo = new MyClass();
foo.some_property;  // Returns 'my value is a string'
foo['some_property'];  // Returns 'my value is a string'
foo.another_property;  // Returns  'i am also a string'
foo['another_property'];  // Also returns 'i am also a string'
foo.0;  // Syntax Error
foo[0];  // Returns 1
foo['0'];  // Returns 1
Brandon McKinney
  • 1,412
  • 11
  • 9
  • 2
    i would definitely not recommend this for performance reasons, but it's the only actual solution to the problem here. Perhaps an edit stating that it's not possible would make this a great answer. – Milimetric Dec 01 '12 at 06:36
  • 8
    This **isn't** what the question wants. The question is asking for a way to catch `foo['random']` which your code is not able to do. – Pacerier Nov 23 '17 at 06:34
14

Use a proxy. It was mentioned elsewhere in the answers but I think that this is a better example:

var handler = {
    get: function(target, name) {
        if (name in target) {
            return target[name];
        }
        if (name == 'length') {
            return Infinity;
        }
        return name * name;
    }
};
var p = new Proxy({}, handler);

p[4]; //returns 16, which is the square of 4.
Eyal
  • 5,728
  • 7
  • 43
  • 70
  • Probably worth mentioning that proxies are an ES6 feature and therefore have a more limited [browser support](https://kangax.github.io/compat-table/es6/#Proxy) (and [Babel cannot fake them either](https://babeljs.io/learn-es2015/#ecmascript-2015-features-proxies)). – jdehesa Jun 17 '17 at 20:36
14

We can proxy get | set methods directly. Inspired by this.

class Foo {
    constructor(v) {
        this.data = v
        return new Proxy(this, {
            get: (obj, key) => {
                if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
                    return obj.data[key]
                else 
                    return obj[key]
            },
            set: (obj, key, value) => {
                if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
                    return obj.data[key] = value
                else 
                    return obj[key] = value
            }
        })
    }
}

var foo = new Foo([])

foo.data = [0, 0, 0]
foo[0] = 1
console.log(foo[0]) // 1
console.log(foo.data) // [1, 0, 0]
Jaber
  • 311
  • 2
  • 5
  • 1
    Other than I don't understand why you're requiring the key to be a string, this seems like the only answer here that actually accomplishes what the OP is after. – Emperor Eto May 24 '22 at 18:51
  • Oops nevermind my ignorant comment about the string key. But does this kill performance vs. a true array? – Emperor Eto May 24 '22 at 20:47
9

As brackets operator is actually property access operator, you can hook on it with getters and setters. For IE you will have to use Object.defineProperty() instead. Example:

var obj = {
    get attr() { alert("Getter called!"); return 1; },
    set attr(value) { alert("Setter called!"); return value; }
};

obj.attr = 123;

The same for IE8+:

Object.defineProperty("attr", {
    get: function() { alert("Getter called!"); return 1; },
    set: function(value) { alert("Setter called!"); return value; }
});

For IE5-7 there's onpropertychange event only, which works for DOM elements, but not for other objects.

The drawback of the method is you can only hook on requests to predefined set of properties, not on arbitrary property without any predefined name.

kstep
  • 1,983
  • 1
  • 15
  • 14
  • Could you please demo your approach on http://jsfiddle.net/ ? I assume that solution should work for any key in expression `obj['any_key'] = 123;` but what I see in your code I need to define setter/getter for any (not yet known) key. That is impossible. – dma_k Nov 05 '13 at 12:50
  • 3
    plus 1 to offset the minus 1 because this is not IE only. – orb Apr 18 '15 at 21:10
  • Could this be done for a class function? I'm struggling to find the syntax for that on my own. – Michael Hoffmann Mar 04 '17 at 23:15
  • It seems this can actually be called on an arbitrary property: `var a = new Array(2);` `function trap_indexing(obj,index) {` `Object.defineProperty(obj,index,{` `get() {` `console.log("getting");` `return this['_shadow'+index];` `},` `set(p) {` `console.log("setting");` `this['_shadow'+index] = p;` `}` `});` `}` `trap_indexing(a,0);` `trap_indexing(a,1);` `trap_indexing(a,2);` `a[0] = 'barf';` `console.log(a[0]);` `a[1] = 'cat';` `console.log(a[1]);` – mondaugen Aug 05 '21 at 16:11
7

one sneaky way to do this is by extending the language itself.

step 1

define a custom indexing convention, let's call it, "[]".

var MyClass = function MyClass(n) {
    this.myArray = Array.from(Array(n).keys()).map(a => 0);
};
Object.defineProperty(MyClass.prototype, "[]", {
    value: function(index) {
        return this.myArray[index];
    }
});

...

var foo = new MyClass(1024);
console.log(foo["[]"](0));

step 2

define a new eval implementation. (don't do this this way, but it's a proof of concept).

var MyClass = function MyClass(length, defaultValue) {
    this.myArray = Array.from(Array(length).keys()).map(a => defaultValue);
};
Object.defineProperty(MyClass.prototype, "[]", {
    value: function(index) {
        return this.myArray[index];
    }
});

var foo = new MyClass(1024, 1337);
console.log(foo["[]"](0));

var mini_eval = function(program) {
    var esprima = require("esprima");
    var tokens = esprima.tokenize(program);
    
    if (tokens.length == 4) {    
        var types = tokens.map(a => a.type);
        var values = tokens.map(a => a.value);
        if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
            if (values[1] == '[' && values[3] == ']') {
                var target = eval(values[0]);
                var i = eval(values[2]);
                // higher priority than []                
                if (target.hasOwnProperty('[]')) {
                    return target['[]'](i);
                } else {
                    return target[i];
                }
                return eval(values[0])();
            } else {
                return undefined;
            }
        } else {
            return undefined;
        }
    } else {
        return undefined;
    }    
};

mini_eval("foo[33]");

the above won't work for more complex indexes but it can be with stronger parsing.

alternative:

instead of resorting to creating your own superset language, you can instead compile your notation to the existing language, then eval it. This reduces the parsing overhead to native after the first time you use it.

var compile = function(program) {
    var esprima = require("esprima");
    var tokens = esprima.tokenize(program);
    
    if (tokens.length == 4) {    
        var types = tokens.map(a => a.type);
        var values = tokens.map(a => a.value);
        if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
            if (values[1] == '[' && values[3] == ']') {
                var target = values[0];
                var i = values[2];
                // higher priority than []                
                return `
                    (${target}['[]']) 
                        ? ${target}['[]'](${i}) 
                        : ${target}[${i}]`
            } else {
                return 'undefined';
            }
        } else {
            return 'undefined';
        }
    } else {
        return 'undefined';
    }    
};

var result = compile("foo[0]");
console.log(result);
console.log(eval(result));
Community
  • 1
  • 1
Dmytro
  • 5,068
  • 4
  • 39
  • 50
  • Cleverness usually is in one way or another. Doesn't mean it's not a worthwhile exercise that can pay off with enough resources. The laziest people are those that write their own compilers and translators just so they can work in more familiar environments, even if they're not available. That said, it would be less disgusting if it was written in less of a hurry by somebody with more experience in the area. All nontrivial solutions are disgusting in one way or another, our job is knowing the tradeoffs and coping with consequences. – Dmytro Jul 27 '19 at 02:52
7

You need to use Proxy as explained, but it can ultimately be integrated into a class constructor

return new Proxy(this, {
    set: function( target, name, value ) {
...}};

with 'this'. Then the set and get (also deleteProperty) functions will fire. Although you get a Proxy object which seems different it for the most part works to ask the compare ( target.constructor === MyClass ) it's class type etc. [even though it's a function where target.constructor.name is the class name in text (just noting an example of things that work slightly different.)]

Master James
  • 1,691
  • 15
  • 19
  • 1
    This works well for overloading the [] on a Backbone collection so that the individual model objects are returned on using [] with a pass-through for all other properties. – justkt Oct 26 '17 at 15:15
4

So you're hoping to do something like var whatever = MyClassInstance[4]; ? If so, simple answer is that Javascript does not currently support operator overloading.

Matt Molnar
  • 2,412
  • 3
  • 22
  • 28
  • 1
    So how does jQuery work. You could call a method on the jQuery object like $('.foo').html() or get the first matching dom element like $('.foo')[0] – kagronick Feb 20 '16 at 16:28
  • 2
    jQuery is a function, you are passing a parameter to the $ function. Hence the () brackets, not [] – James Westgate Apr 18 '16 at 15:44
1

Have a look at Symbol.iterator. You can implement a user-defined @@iterator method to make any object iterable.

The well-known Symbol.iterator symbol specifies the default iterator for an object. Used by for...of.

Example:

class MyClass {

  constructor () {
    this._array = [data]
  }

  *[Symbol.iterator] () {
    for (let i=0, n=this._array.length; i<n; i++) {
      yield this._array[i]
    }
  }
}

const c = new MyClass()

for (const element of [...c]) {
  // do something with element
}
0

class and proxy can be combined:

class Overload {
    #data = {};
    constructor () {
        return new Proxy(this, this);
    }
    get (_, p) {
        return this.#data[p];
    }
}

remember that proxy handles all prop access.

Ali
  • 21,572
  • 15
  • 83
  • 95