web-dev-qa-db-fra.com

Est-il possible d'implémenter des getters / setters dynamiques en JavaScript?

Je sais comment créer des getters et des setters pour des propriétés dont on connaît déjà les noms, en procédant comme ceci:

// A trivial example:
function MyObject(val){
    this.count = 0;
    this.value = val;
}
MyObject.prototype = {
    get value(){
        return this.count < 2 ? "Go away" : this._value;
    },
    set value(val){
        this._value = val + (++this.count);
    }
};
var a = new MyObject('foo');

alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"

Maintenant, ma question est la suivante: est-il possible de définir une sorte d’attaquants et de passeurs comme ceux-là? C'est-à-dire, créez des accesseurs et des setters pour tout nom de propriété qui n'est pas déjà défini.

Le concept est possible dans PHP en utilisant les méthodes magiques __get() et __set() _) (voir the PHP = documentation pour plus d'informations à ce sujet), je me demande donc s'il existe un équivalent JavaScript à ceux-ci?

Inutile de dire que je préférerais idéalement une solution compatible avec plusieurs navigateurs.

117
megaflop

Mise à jour 2013 et 2015 (voir ci-dessous la réponse d'origine de 2011):

Cela a changé à partir de la spécification ES2015 (alias "ES6"): JavaScript a maintenant proxies . Les proxies vous permettent de créer des objets qui sont de véritables proxies (façades) sur d'autres objets. Voici un exemple simple qui convertit toutes les valeurs de propriété qui sont des chaînes en majuscules lors de l'extraction:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    "foo": "bar"
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        let rv = Reflect.get(target, name, receiver);
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log(`original.foo = ${original.foo}`); // "original.foo = bar"
console.log(`proxy.foo = ${proxy.foo}`);       // "proxy.foo = BAR"

Les opérations que vous ne remplacez pas ont leur comportement par défaut. Dans ce qui précède, tout ce que nous remplaçons est get, mais il existe toute une liste d'opérations auxquelles vous pouvez vous connecter.

Dans la liste des arguments de la fonction get du gestionnaire:

  • target est l'objet traité par proxy (original, dans notre cas).
  • name est (bien sûr) le nom de la propriété en cours de récupération, qui est généralement une chaîne mais peut également être un symbole.
  • receiver est l'objet qui doit être utilisé comme this dans la fonction getter si la propriété est un accesseur plutôt qu'une propriété de données. En temps normal, c'est le proxy ou quelque chose qui en hérite, mais peut ​​être n'importe quoi puisque le piège peut être déclenché par Reflect.get.

Cela vous permet de créer un objet avec la fonctionnalité get-and-getter all-catch que vous souhaitez:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
    get(target, name, receiver) {
        if (!Reflect.has(target, name)) {
            console.log("Getting non-existent property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set(target, name, value, receiver) {
        if (!Reflect.has(target, name)) {
            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
        }
        return Reflect.set(target, name, value, receiver);
    }
});

console.log(`[before] obj.foo = ${obj.foo}`);
obj.foo = "bar";
console.log(`[after] obj.foo = ${obj.foo}`);

La sortie de ce qui précède est:

Obtenir la propriété non existante 'foo' 
 [Before] obj.foo = undefined 
 Définition de la propriété non existante 'foo', valeur initiale: bar 
 [Après] obj.foo = bar

Notez comment nous obtenons le message "non-existant" lorsque nous essayons de récupérer foo quand il n'existe pas encore, et encore lorsque nous le créons, mais pas après.


Réponse de 2011 (voir ci-dessus pour les mises à jour de 2013 et 2015):

Non, JavaScript n'a pas de fonction fourre-tout. La syntaxe de l'accesseur que vous utilisez est décrite dans la section 11.1.5 de la spécification et n'offre aucun caractère générique ou quelque chose du genre.

Vous pouvez bien sûr implémenter une fonction pour le faire, mais je suppose que vous ne voulez probablement pas utiliser f = obj.prop("foo"); plutôt que f = obj.foo; Et obj.prop("foo", value); plutôt que obj.foo = value; (qui serait nécessaire pour que la fonction gère des propriétés inconnues).

FWIW, la fonction getter (je ne me suis pas soucié de la logique du setter) ressemblerait à ceci:

MyObject.prototype.prop = function(propName) {
    if (propName in this) {
        // This object or its prototype already has this property,
        // return the existing value.
        return this[propName];
    }

    // ...Catch-all, deal with undefined property here...
};

Mais encore une fois, je ne peux pas imaginer que vous souhaitiez vraiment faire cela, à cause de la façon dont cela change la façon dont vous utilisez l'objet.

188
T.J. Crowder

En Javascript moderne (FF4 +, IE9 +, Chrome 5+, Safari 5.1+, Opera 11.60+)], il y a Object.defineProperty. Cet exemple sur le MDN explique très bien le fonctionnement de defineProperty et rend possibles les getters et les setters dynamiques.

Techniquement, cela ne fonctionnera pas sur les requêtes dynamiques que vous cherchez, mais si vos getters et setters valides sont définis par, par exemple, un AJAX appel à un serveur JSON-RPC, par exemple, vous pouvez l’utiliser de la manière suivante:

arrayOfNewProperties.forEach(function(property) {
    Object.defineProperty(myObject, property.name, {
        set: property.setter, get: property.getter
    });
});
47
David Ellis

Ce qui suit pourrait être une approche originale de ce problème:

var obj = {
  emptyValue: null,
  get: function(prop){
    if(typeof this[prop] == "undefined")
        return this.emptyValue;
    else
        return this[prop];
  },
  set: function(prop,value){
    this[prop] = value;
  }
}

Pour pouvoir l'utiliser, les propriétés doivent être transmises sous forme de chaînes. Alors, voici un exemple de la façon dont cela fonctionne:

//To set a property
obj.set('myProperty','myValue');

//To get a property
var myVar = obj.get('myProperty');

Edit: Une approche améliorée, plus orientée objet, basée sur ce que j'ai proposé est la suivante:

function MyObject() {
    var emptyValue = null;
    var obj = {};
    this.get = function(prop){
        return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
    };
    this.set = function(prop,value){
        obj[prop] = value;
    };
}

var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));

Vous pouvez le voir fonctionner ici .

6
clami219