Comment surchargeriez-vous l'opérateur [] en javascript

85

Je n'arrive pas à trouver le moyen de surcharger l'opérateur [] en javascript. Quelqu'un sait-il?

Je pensais aux lignes de ...

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

ou est-ce que je ne regarde pas les bonnes choses.

slypride
la source
3
Les réponses ici sont fausses, les tableaux dans JS ne sont que des objets dont les clés peuvent être forcées à des valeurs uint32 (- 1) et ont des méthodes supplémentaires sur leur prototype
Benjamin Gruenbaum
Faites simplement de votre MyClassobjet un tableau. Vous pouvez copier les clés et les valeurs de myArrayvers votre var myObj = new MyClass()objet.
jpaugh
hé, je voudrais surcharger l'opérateur {}, une idée?
daniel assayag le

Réponses:

81

Vous ne pouvez pas surcharger les opérateurs dans JavaScript.

Il a été proposé pour ECMAScript 4 mais rejeté.

Je ne pense pas que vous le verrez de si tôt.

Peter Bailey
la source
4
Cela peut déjà être faisable avec des proxys dans certains navigateurs et arrivera à tous les navigateurs à un moment donné. Voir github.com/DavidBruant/ProxyArray
Tristan
Alors, comment jQuery retourne-t-il différentes choses selon que vous utilisez [] ou .eq ()? stackoverflow.com/a/6993901/829305
Rikki
2
Vous pouvez le faire maintenant avec un proxy.
Eyal
bien que vous puissiez définir des méthodes avec des symboles en eux tant que vous y accédez sous forme de tableau plutôt qu'avec ".". C'est ainsi que SmallTalk mappe des choses comme Object arg1: a arg2: b arg3: cas Object["arg1:arg2:arg3:"](a,b,c). Vous pouvez donc avoir myObject["[]"](1024): P
Dmitry
Le lien est mort :(
Gp2mv3
69

Vous pouvez le faire avec ES6 Proxy (disponible dans tous les navigateurs modernes)

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

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

Vérifiez les détails sur MDN .

Joe moyen
la source
2
Comment utiliserions-nous cela pour créer notre propre classe avec un accesseur d'index? c'est-à-dire que je veux utiliser mon propre constructeur, je ne veux pas construire de proxy.
mpen
1
Ce n'est pas une vraie surcharge. Au lieu d'appeler des méthodes sur l'objet lui-même, vous appelez maintenant les méthodes du proxy.
Pacerier
@Pacerier vous pouvez revenir target[name]dans le getter, OP montre juste les exemples
Valen
1
Fonctionne également avec l' []opérateur, btw:var key = 'world'; console.log(proxy[key]);
Klesun
15

La réponse simple est que JavaScript permet d'accéder aux enfants d'un objet via les crochets.

Vous pouvez donc définir votre classe:

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;
};

Vous pourrez alors accéder aux membres sur toutes les instances de votre classe avec l'une ou l'autre syntaxe.

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
la source
2
Je ne recommanderais certainement pas cela pour des raisons de performances, mais c'est la seule solution réelle au problème ici. Peut-être qu'une modification indiquant que ce n'est pas possible en ferait une excellente réponse.
Milimetric
4
Ce n'est pas ce que veut la question. La question est de savoir comment saisir foo['random']ce que votre code n'est pas capable de faire.
Pacerier
9

Utilisez un proxy. Cela a été mentionné ailleurs dans les réponses mais je pense que c'est un meilleur exemple:

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
la source
Il convient probablement de mentionner que les proxys sont une fonctionnalité ES6 et ont donc un support de navigateur plus limité (et Babel ne peut pas non plus les simuler ).
jdehesa
8

Comme l'opérateur de parenthèses est en fait un opérateur d'accès à la propriété, vous pouvez l'accrocher avec des getters et des setters. Pour IE, vous devrez utiliser Object.defineProperty () à la place. Exemple:

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

obj.attr = 123;

Idem pour IE8 +:

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

Pour IE5-7, il n'y a que l' onpropertychangeévénement, qui fonctionne pour les éléments DOM, mais pas pour les autres objets.

L'inconvénient de la méthode est que vous ne pouvez accrocher que les requêtes à un ensemble de propriétés prédéfini, pas à une propriété arbitraire sans nom prédéfini.

kstep
la source
Pourriez-vous s'il vous plaît faire une démonstration de votre approche sur jsfiddle.net ? Je suppose que cette solution devrait fonctionner pour n'importe quelle clé dans l'expression, obj['any_key'] = 123;mais ce que je vois dans votre code, j'ai besoin de définir setter / getter pour toute clé (pas encore connue). C'est impossible.
dma_k
3
plus 1 pour compenser le moins 1 car ce n'est pas seulement IE.
orbe
Cela pourrait-il être fait pour une fonction de classe? J'ai du mal à trouver la syntaxe pour cela par moi-même.
Michael Hoffmann
7

Vous devez utiliser Proxy comme expliqué, mais il peut finalement être intégré dans un constructeur de classe

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

avec ça'. Ensuite, les fonctions set et get (également deleteProperty) se déclenchent. Bien que vous obteniez un objet Proxy qui semble différent, il fonctionne pour la plupart de demander à comparer (target.constructor === MyClass) son type de classe, etc. [même si c'est une fonction où target.constructor.name est le nom de la classe dans texte (en notant simplement un exemple de choses qui fonctionnent légèrement différemment.)]

Maître James
la source
1
Cela fonctionne bien pour surcharger le [] sur une collection Backbone afin que les objets de modèle individuels soient renvoyés lors de l'utilisation de [] avec un pass-through pour toutes les autres propriétés.
justkt
5

Vous espérez donc faire quelque chose comme var n'importe quoi = MyClassInstance [4]; ? Si tel est le cas, la réponse simple est que Javascript ne prend actuellement pas en charge la surcharge des opérateurs.

Matt Molnar
la source
1
Alors, comment fonctionne jQuery. Vous pouvez appeler une méthode sur l'objet jQuery comme $ ('. Foo'). Html () ou obtenir le premier élément dom correspondant comme $ ('. Foo') [0]
kagronick
1
jQuery est une fonction, vous passez un paramètre à la fonction $. D'où les crochets (), pas []
James Westgate
5

une façon sournoise de le faire est d'étendre le langage lui-même.

étape 1

définir une convention d'indexation personnalisée, appelons-la, "[]".

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));

étape 2

définir une nouvelle implémentation eval. (ne faites pas cela de cette façon, mais c'est une preuve de 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]");

ce qui précède ne fonctionnera pas pour des index plus complexes, mais cela peut l'être avec une analyse plus poussée.

alternative:

au lieu de recourir à la création de votre propre langage de surensemble, vous pouvez à la place compiler votre notation dans le langage existant, puis l'évaluer. Cela réduit la surcharge d'analyse en natif après la première utilisation.

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));
Dmitry
la source
1
Vous êtes tordu +1
Jus
Absolument dégoûtant. J'aime cela.
SliceThePi
L'intelligence est généralement d'une manière ou d'une autre. Cela ne veut pas dire que ce n'est pas un exercice valable qui peut porter ses fruits avec suffisamment de ressources. Les personnes les plus paresseuses sont celles qui écrivent leurs propres compilateurs et traducteurs pour pouvoir travailler dans des environnements plus familiers, même s'ils ne sont pas disponibles. Cela dit, ce serait moins dégoûtant s'il était rédigé moins rapidement par quelqu'un ayant plus d'expérience dans le domaine. Toutes les solutions non triviales sont dégoûtantes d'une manière ou d'une autre, notre travail consiste à connaître les compromis et à faire face aux conséquences.
Dmitry
3

Nous pouvons obtenir un proxy | définir des méthodes directement. Inspiré par cela .

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
la source