How would you overload the [] operator in javascript
Asked Answered
H

11

107

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.

Hootenanny answered 10/11, 2009 at 21:31 Comment(3)
The answers here are wrong, Arrays in JS are just objects whose keys are coercable to uint32 ( - 1) values and have extra methods on their prototypeSsw
Just make your MyClass object an array. You can copy the keys and values from myArray to your var myObj = new MyClass() object.Mimetic
hey, i'd like overload the {} operator, any idea?Alternative
S
86

This answer is outdated as of ES6. See average Joe's answer for an answer using new features in ES6. However, this answer is still true for browsers that lack ES6 support.

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.

Seventieth answered 10/11, 2009 at 21:37 Comment(7)
This may be doable with proxies in some browsers already, and will be coming to all browsers at some point. See github.com/DavidBruant/ProxyArraySuspect
Then how does jQuery return different things dependent on whether you use [] or .eq()? https://mcmap.net/q/112987/-do-jquery-selectors-return-an-array-of-html-element-or-jquery-objectsUnitarian
You can do it now with a Proxy.Turf
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) :PMcnelly
The link is dead :(Inly
All modern browsers have Proxy now.Gaudy
@Unitarian - By just storing things at those indexes in the object.Gaudy
D
107

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.

Dixiedixieland answered 4/9, 2014 at 6:50 Comment(7)
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.Cirilo
This isn't true overloading. Instead of calling methods on the object itself, you are now calling methods of the proxy.Anole
@Anole you can return target[name] in the getter, OP is just showing the examplesPhosphorous
Works with [] operator as well, btw: var key = 'world'; console.log(proxy[key]);Dyslalia
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.Ponton
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.Tip
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.Lurleen
S
86

This answer is outdated as of ES6. See average Joe's answer for an answer using new features in ES6. However, this answer is still true for browsers that lack ES6 support.

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.

Seventieth answered 10/11, 2009 at 21:37 Comment(7)
This may be doable with proxies in some browsers already, and will be coming to all browsers at some point. See github.com/DavidBruant/ProxyArraySuspect
Then how does jQuery return different things dependent on whether you use [] or .eq()? https://mcmap.net/q/112987/-do-jquery-selectors-return-an-array-of-html-element-or-jquery-objectsUnitarian
You can do it now with a Proxy.Turf
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) :PMcnelly
The link is dead :(Inly
All modern browsers have Proxy now.Gaudy
@Unitarian - By just storing things at those indexes in the object.Gaudy
G
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
Gossamer answered 19/4, 2011 at 22:28 Comment(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.Deirdre
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.Anole
P
15

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]
Prefiguration answered 24/8, 2019 at 2:51 Comment(2)
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.Timepiece
Oops nevermind my ignorant comment about the string key. But does this kill performance vs. a true array?Timepiece
T
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.
Turf answered 5/5, 2017 at 18:45 Comment(1)
Probably worth mentioning that proxies are an ES6 feature and therefore have a more limited browser support (and Babel cannot fake them either).Yorke
U
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.

Urbanize answered 21/8, 2012 at 19:12 Comment(4)
Could you please demo your approach on 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.Distaste
plus 1 to offset the minus 1 because this is not IE only.Roana
Could this be done for a class function? I'm struggling to find the syntax for that on my own.Sherrard
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]);Couperin
M
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));
Mcnelly answered 24/7, 2017 at 12:51 Comment(1)
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.Mcnelly
E
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.)]

Eschalot answered 27/9, 2017 at 3:12 Comment(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.Ophicleide
S
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.

Shaeffer answered 10/11, 2009 at 21:35 Comment(2)
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]Cairn
jQuery is a function, you are passing a parameter to the $ function. Hence the () brackets, not []Samuele
C
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
}
Cashbook answered 26/8, 2022 at 14:2 Comment(1)
So this lets you iterate, but can you access arbitrary elements by index?Charity
S
1

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.

Stays answered 3/9, 2023 at 1:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.