È possibile implementare getter / setter dinamici in JavaScript?


132

Sono a conoscenza di come creare getter e setter per proprietà i cui nomi si conoscono già, facendo qualcosa del genere:

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

Ora, la mia domanda è: è possibile definire una sorta di raccoglitori e setter come tutti? Vale a dire, creare getter e setter per qualsiasi nome di proprietà che non sia già definito.

Il concetto è possibile in PHP usando i metodi magici __get()e __set()(vedi la documentazione di PHP per informazioni su questi), quindi sto davvero chiedendo esiste un JavaScript equivalente a questi?

Inutile dire che preferirei una soluzione compatibile con più browser.


Sono riuscito a farlo, vedi la mia risposta qui per come.

Risposte:


216

Aggiornamento 2013 e 2015 (vedi sotto per la risposta originale del 2011) :

Questo è cambiato a partire dalla specifica ES2015 (aka "ES6"): JavaScript ora ha dei proxy . I proxy ti consentono di creare oggetti che sono veri proxy per (facciate su) altri oggetti. Ecco un semplice esempio che trasforma i valori delle proprietà che sono stringhe in tutti i limiti al momento del recupero:

"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"

Le operazioni che non vengono ignorate hanno il comportamento predefinito. In quanto sopra, tutto ciò che sovrascrive è get, ma c'è un intero elenco di operazioni a cui puoi agganciare.

Nell'elenco degli getargomenti della funzione gestore:

  • targetè l'oggetto sottoposto a proxy ( original, nel nostro caso).
  • name è (ovviamente) il nome della proprietà da recuperare, che di solito è una stringa ma potrebbe anche essere un simbolo.
  • receiverè l'oggetto che dovrebbe essere usato come thisnella funzione getter se la proprietà è un accessor anziché una proprietà data. Nel caso normale questo è il proxy o qualcosa che eredita da esso, ma può essere qualsiasi cosa poiché la trap può essere attivata da Reflect.get.

Ciò ti consente di creare un oggetto con la funzione getter e setter che desideri:

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

L'output di quanto sopra è:

Ottenere 'foo' proprietà inesistenti
[before] obj.foo = non definito
Impostazione della proprietà inesistente 'pippo', valore iniziale: bar
[after] obj.foo = bar

Nota come otteniamo il messaggio "inesistente" quando proviamo a recuperare fooquando non esiste ancora, e ancora quando lo creiamo, ma non dopo.


Risposta dal 2011 (vedi sopra per gli aggiornamenti 2013 e 2015) :

No, JavaScript non ha una funzione di proprietà generale. La sintassi del programma di accesso che stai utilizzando è descritta nella Sezione 11.1.5 delle specifiche e non offre caratteri jolly o simili.

Ovviamente potresti implementare una funzione per farlo, ma suppongo che probabilmente non vuoi usare f = obj.prop("foo");piuttosto che f = obj.foo;e obj.prop("foo", value);piuttosto che obj.foo = value;(che sarebbe necessario per la funzione per gestire proprietà sconosciute).

FWIW, la funzione getter (non mi sono preoccupato della logica setter) sarebbe simile a questa:

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

Ma ancora una volta, non riesco a immaginare che vorresti davvero farlo, a causa di come cambia il modo in cui usi l'oggetto.


1
C'è un'alternativa a Proxy: Object.defineProperty(). Ho inserito i dettagli nella mia nuova risposta .
Andrew

@Andrew - Temo che tu abbia letto male la domanda, vedi il mio commento sulla tua risposta.
TJ Crowder,

4

Di seguito potrebbe essere un approccio originale a questo problema:

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

Per usarlo le proprietà devono essere passate come stringhe. Quindi, ecco un esempio di come funziona:

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

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

Modifica: un approccio migliorato e più orientato agli oggetti basato su ciò che ho proposto è il seguente:

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

Puoi vederlo funzionare qui .


Questo non funziona Non è possibile definire un getter senza specificare il nome della proprietà.
John Kurlak,

@JohnKurlak Controlla questo jsFiddle: jsfiddle.net/oga7ne4x Funziona. Devi solo passare i nomi delle proprietà come stringhe.
clami219,

3
Ah, grazie per il chiarimento. Pensavo stessi provando a usare la funzione di linguaggio get () / set (), non a scrivere il tuo get () / set (). Non mi piace ancora questa soluzione perché non risolve davvero il problema originale.
John Kurlak,

@JohnKurlak Bene, ho scritto che è un approccio originale. Fornisce un modo diverso di risolvere il problema, anche se non risolve il problema in cui si dispone di un codice esistente che utilizza un approccio più tradizionale. Ma va bene se stai iniziando da zero. Sicuramente non degno un
voto negativo

@JohnKurlak Vedi se ora sembra migliore! :)
clami219

0

Prefazione:

La risposta di TJ Crowder menziona a Proxy, che sarà necessario per un getter / setter generico per proprietà che non esistono, come richiesto dall'OP. A seconda di quale comportamento si desidera effettivamente con i getter / setter dinamici, Proxypotrebbe non essere effettivamente necessario; o, potenzialmente, potresti voler usare una combinazione di a Proxycon quello che ti mostrerò di seguito.

(PS Di Proxyrecente ho sperimentato a fondo in Firefox su Linux e l'ho trovato molto capace, ma anche un po 'confuso / difficile da lavorare e ottenere correttamente. Ancora più importante, ho anche trovato che è abbastanza lento (almeno in in relazione a come JavaScript ottimizzato tende ad essere oggigiorno) - Sto parlando nel regno dei deca-multipli più lentamente.)


Per implementare specificamente getter e setter creati dinamicamente, puoi usare Object.defineProperty()o Object.defineProperties(). Anche questo è abbastanza veloce.

L'essenza è che puoi definire un getter e / o setter su un oggetto in questo modo:

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.

Diverse cose da notare qui:

  • Non è possibile utilizzare la valueproprietà nel descrittore di proprietà ( non mostrato sopra) contemporaneamente a gete / o set; dai documenti:

    I descrittori di proprietà presenti negli oggetti sono disponibili in due tipi principali: descrittori di dati e descrittori di accessori. Un descrittore di dati è una proprietà che ha un valore, che può essere o meno scrivibile. Un descrittore di accesso è una proprietà descritta da una coppia di funzioni getter-setter. Un descrittore deve essere uno di questi due gusti; non può essere entrambi.

  • Pertanto, noterai che ho creato una valproprietà al di fuori del Object.defineProperty()descrittore di chiamata / proprietà. Questo è un comportamento standard.
  • Come per l'errore qui , non impostare writablesu truenel descrittore di proprietà se si utilizza geto set.
  • Potresti voler considerare l'impostazione configurablee enumerable, tuttavia, a seconda di cosa stai cercando; dai documenti:

    configurabile

    • true se e solo se il tipo di questo descrittore di proprietà può essere modificato e se la proprietà può essere eliminata dall'oggetto corrispondente.

    • Il valore predefinito è false.


    enumerabile

    • true se e solo se questa proprietà viene visualizzata durante l'enumerazione delle proprietà sull'oggetto corrispondente.

    • Il valore predefinito è false.


In questa nota, questi possono anche essere di interesse:


2
Temo che tu abbia letto male la domanda. L'OP ha chiesto specificamente di catturare tutti come PHP __gete__set . definePropertynon gestisce quel caso. Dalla domanda: "Vale a dire, creare getter e setter per qualsiasi nome di proprietà che non sia già definito." (la loro enfasi). definePropertydefinisce le proprietà in anticipo. L'unico modo per fare ciò che l'OP ha richiesto è un proxy.
TJ Crowder,

@TJCrowder vedo. Tecnicamente hai ragione, anche se la domanda non era molto chiara. Ho adattato la mia risposta di conseguenza. Inoltre, alcuni potrebbero effettivamente desiderare una combinazione delle nostre risposte (lo faccio personalmente).
Andrew

@Andrew quando ho fatto questa domanda nel 2011, il caso d'uso che avevo in mente era una libreria in grado di restituire un oggetto su cui l'utente poteva chiamare in modo obj.whateverPropertytale che la libreria potesse intercettarlo con un generico getter e ottenere il nome della proprietà l'utente ha tentato di accedere. Da qui il requisito di "catcher all getter and setter".
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)

questo funziona per me


13
Usare Function()così è come usare eval. Basta mettere direttamente le funzioni come parametri di defineProperty. Oppure, se per qualche motivo insisti nel creare dinamicamente gete set, quindi, utilizzare una funzione di ordine superiore che crea la funzione e la restituisce, comevar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.