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

132

Je sais comment créer des getters et des setters pour des propriétés dont on connaît déjà les noms, en faisant quelque chose 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 de getters et de setters fourre-tout comme ceux-ci? Par exemple, créez des getters et des setters pour tout nom de propriété qui n'est pas déjà défini.

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

Inutile de dire que j'aimerais idéalement une solution compatible avec tous les navigateurs.

marguerite
la source
J'ai réussi à le faire, voir ma réponse ici pour savoir comment.

Réponses:

216

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

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

"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 y a toute une liste d'opérations auxquelles vous pouvez vous connecter.

Dans la getliste des arguments de la fonction de gestionnaire:

  • targetest l'objet mandaté ( originaldans 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.
  • receiverest l'objet qui doit être utilisé comme thisdans la fonction getter si la propriété est un accesseur plutôt qu'une propriété de données. Dans le cas normal, c'est le proxy ou quelque chose qui en hérite, mais cela peut être n'importe quoi puisque le trap peut être déclenché par Reflect.get.

Cela vous permet de créer un objet avec la fonction catch-all getter et setter que vous voulez:

"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}`);

Le résultat de ce qui précède est:

Obtenir la propriété inexistante «foo»
[avant] obj.foo = indéfini
Définition de la propriété inexistante 'foo', valeur initiale: bar
[après] obj.foo = bar

Notez comment nous obtenons le message "inexistant" lorsque nous essayons de le récupérer fooalors qu'il n'existe pas encore, et à nouveau lorsque nous le créons, mais pas après cela.


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

Non, JavaScript n'a pas de fonction de propriété fourre-tout. La syntaxe d'accesseur que vous utilisez est traitée dans la section 11.1.5 de la spécification, et n'offre aucun caractère générique ou quelque chose comme ça.

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;(ce 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.

TJ Crowder
la source
1
Il y a une alternative à Proxy: Object.defineProperty(). J'ai mis les détails dans ma nouvelle réponse .
Andrew le
@Andrew - J'ai bien peur que vous ayez mal lu la question, voyez mon commentaire sur votre réponse.
TJ Crowder le
4

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 l'utiliser, les propriétés doivent être passées sous forme de chaînes. Voici donc un exemple de son fonctionnement:

//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 .

clami219
la source
Cela ne marche pas. Vous ne pouvez pas définir un getter sans spécifier le nom de la propriété.
John Kurlak
@JohnKurlak Vérifiez ce jsFiddle: jsfiddle.net/oga7ne4x Cela fonctionne. Il vous suffit de transmettre les noms de propriétés sous forme de chaînes.
clami219
3
Ah, merci d'avoir clarifié. Je pensais que vous essayiez d'utiliser la fonction de langage get () / set (), sans écrire votre propre get () / set (). Je n'aime toujours pas cette solution car elle ne résout pas vraiment le problème d'origine.
John Kurlak
@JohnKurlak Eh bien, j'ai écrit que c'est une approche originale. Il fournit une manière différente de résoudre le problème, même s'il ne résout pas le problème où vous avez un code existant qui utilise une approche plus traditionnelle. Mais c'est bien si vous partez de zéro. Sûrement pas digne d'un
vote négatif
@JohnKurlak Voyez si maintenant ça a l'air mieux! :)
clami219
0

Préface:

La réponse de TJ Crowder mentionne a Proxy, qui sera nécessaire pour un getter / setter fourre-tout pour les propriétés qui n'existent pas, comme l'OP le demandait. En fonction du comportement réellement recherché avec les getters / setters dynamiques, a Proxypeut ne pas être nécessaire cependant; ou, potentiellement, vous voudrez peut-être utiliser une combinaison de a Proxyavec ce que je vais vous montrer ci-dessous.

(PS J'ai Proxyrécemment expérimenté à fond dans Firefox sur Linux et je l'ai trouvé très performant, mais aussi quelque peu déroutant / difficile à utiliser et à faire correctement. Plus important encore, je l'ai également trouvé assez lent (du moins en rapport à la façon dont JavaScript a tendance à être optimisé de nos jours) - je parle dans le domaine des déca-multiples plus lents.)


Pour implémenter spécifiquement des getters et des setters créés dynamiquement, vous pouvez utiliser Object.defineProperty()ou Object.defineProperties(). C'est également assez rapide.

L'essentiel est que vous pouvez définir un getter et / ou un setter sur un objet comme ceci:

let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
  //Alternatively, use: `get() {}`
  get: function() {
    return val;
  },
  //Alternatively, use: `set(newValue) {}`
  set: function(newValue) {
    val = newValue;
  }
});

//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.

//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.

Plusieurs choses à noter ici:

  • Vous ne pouvez pas utiliser la valuepropriété dans le descripteur de propriété ( non illustré ci-dessus) simultanément avec getet / ou set; à partir de la documentation:

    Les descripteurs de propriété présents dans les objets se présentent sous deux formes principales: les descripteurs de données et les descripteurs d'accesseurs. Un descripteur de données est une propriété qui a une valeur, qui peut ou non être inscriptible. Un descripteur d'accesseur est une propriété décrite par une paire de fonctions getter-setter. Un descripteur doit être l'une de ces deux saveurs; ce ne peut pas être les deux.

  • Ainsi, vous noterez que j'ai créé une valpropriété en dehors du Object.defineProperty()descripteur d'appel / de propriété. C'est un comportement standard.
  • Conformément à l'erreur ici , ne définissez pas writablesur truedans le descripteur de propriété si vous utilisez getou set.
  • Vous voudrez peut-être envisager de définir configurableet enumerable, cependant, en fonction de ce que vous recherchez; à partir de la documentation:

    configurable

    • true si et seulement si le type de ce descripteur de propriété peut être modifié et si la propriété peut être supprimée de l'objet correspondant.

    • La valeur par défaut est false.


    énumérable

    • true si et seulement si cette propriété apparaît lors de l'énumération des propriétés sur l'objet correspondant.

    • La valeur par défaut est false.


Sur cette note, ceux-ci peuvent également être intéressants:

  • Object.getOwnPropertyNames(obj): récupère toutes les propriétés d'un objet, même celles qui ne sont pas énumérables (AFAIK c'est la seule façon de le faire!).
  • Object.getOwnPropertyDescriptor(obj, prop): obtient le descripteur de propriété d'un objet, l'objet qui a été passé Object.defineProperty()ci-dessus.
  • obj.propertyIsEnumerable(prop);: pour une propriété individuelle sur une instance d'objet spécifique, appelez cette fonction sur l'instance d'objet pour déterminer si la propriété spécifique est énumérable ou non.
Andrew
la source
2
J'ai bien peur que vous ayez mal lu la question. L'OP a spécifiquement demandé des captures tout comme PHP __getet__set . definePropertyne gère pas ce cas. De la question: "Ie, créez des getters et des setters pour tout nom de propriété qui n'est pas déjà défini." (leur emphase). definePropertydéfinit les propriétés à l'avance. La seule façon de faire ce que l'OP a demandé est un proxy.
TJ Crowder le
@TJCrowder Je vois. Vous avez techniquement raison, même si la question n'était pas très claire. J'ai ajusté ma réponse en conséquence. En outre, certains voudront peut-être une combinaison de nos réponses (je le fais personnellement).
Andrew le
@Andrew lorsque j'ai posé cette question en 2011, le cas d'utilisation que j'avais à l'esprit était une bibliothèque qui peut renvoyer un objet sur lequel l'utilisateur pourrait appeler de obj.whateverPropertysorte que la bibliothèque puisse intercepter cela avec un getter générique et recevoir le nom de propriété l'utilisateur a essayé d'accéder. D'où l'exigence de «getters et setters fourre-tout».
daiscog
-6
var x={}
var propName = 'value' 
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)

cela fonctionne pour moi

Bruno
la source
13
Utiliser Function()comme ça, c'est comme utiliser eval. Il suffit de mettre directement les fonctions comme paramètres de defineProperty. Ou, si pour une raison quelconque vous insistez pour créer dynamiquement getet set, alors utilisez une fonction d'ordre élevé qui crée la fonction et la renvoie, commevar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l