¿Es posible implementar getters / setters dinámicos en JavaScript?

132

Soy consciente de cómo crear captadores y establecedores para propiedades cuyos nombres ya se conocen, haciendo algo como esto:

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

Ahora, mi pregunta es, ¿es posible definir una especie de captadores y establecedores generales como estos? Es decir, crear captadores y establecedores para cualquier nombre de propiedad que no esté definido.

El concepto es posible en PHP utilizando los métodos __get()y __set()magic (consulte la documentación de PHP para obtener información sobre estos), por lo que realmente pregunto si hay un equivalente de JavaScript para estos.

No hace falta decir que, idealmente, me gustaría una solución que sea compatible con varios navegadores.

daiscog
fuente
Logré hacerlo, mira mi respuesta aquí para saber cómo.

Respuestas:

216

Actualización de 2013 y 2015 (ver abajo la respuesta original de 2011) :

Esto cambió a partir de la especificación ES2015 (también conocido como "ES6"): JavaScript ahora tiene proxies . Los servidores proxy le permiten crear objetos que son servidores proxy verdaderos para (fachadas) de otros objetos. Aquí hay un ejemplo simple que convierte los valores de propiedad que son cadenas en mayúsculas en la recuperación:

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

Las operaciones que no anula tienen su comportamiento predeterminado. En lo anterior, todo lo que anulamos es get, pero hay una lista completa de operaciones en las que puede engancharse.

En la getlista de argumentos de la función del controlador:

  • target es el objeto que se representa (original en nuestro caso).
  • name es (por supuesto) el nombre de la propiedad que se está recuperando, que generalmente es una cadena pero también podría ser un Símbolo.
  • receiveres el objeto que debe usarse como thisen la función getter si la propiedad es un descriptor de acceso en lugar de una propiedad de datos. En el caso normal, este es el proxy o algo que hereda de él, pero puede ser cualquier cosa ya que la trampa puede ser activada por Reflect.get.

Esto le permite crear un objeto con la función de captador y configurador general que desea:

"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 salida de lo anterior es:

Obtención de propiedad inexistente 'foo'
[antes] obj.foo = undefined
Configuración de la propiedad inexistente 'foo', valor inicial: bar
[después] obj.foo = bar

Observe cómo obtenemos el mensaje "inexistente" cuando intentamos recuperarlo foocuando aún no existe, y nuevamente cuando lo creamos, pero no después de eso.


Respuesta de 2011 (ver arriba para actualizaciones de 2013 y 2015) :

No, JavaScript no tiene una característica de propiedad general. La sintaxis del descriptor de acceso que está utilizando se trata en la Sección 11.1.5 de la especificación, y no ofrece ningún comodín o algo así.

Por supuesto, podría implementar una función para hacerlo, pero supongo que probablemente no quiera usar en f = obj.prop("foo");lugar de f = obj.foo;y en obj.prop("foo", value);lugar de obj.foo = value;(lo que sería necesario para que la función maneje propiedades desconocidas).

FWIW, la función getter (no me molesté con la lógica del setter) se vería así:

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

Pero nuevamente, no puedo imaginar que realmente quieras hacer eso, debido a cómo cambia la forma en que usas el objeto.

TJ Crowder
fuente
1
Hay una alternativa a Proxy: Object.defineProperty(). Puse los detalles en mi nueva respuesta .
Andrew
@ Andrew - Me temo que has leído mal la pregunta, mira mi comentario sobre tu respuesta.
TJ Crowder
4

Lo siguiente podría ser un enfoque original para este 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;
  }
}

Para usarlo, las propiedades deben pasarse como cadenas. Aquí hay un ejemplo de cómo funciona:

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

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

Editar: un enfoque mejorado y más orientado a objetos basado en lo que propuse es el siguiente:

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

Puedes verlo trabajando aquí .

clami219
fuente
Esto no funciona No puede definir un captador sin especificar el nombre de la propiedad.
John Kurlak
@JohnKurlak Comprueba esto jsFiddle: jsfiddle.net/oga7ne4x Funciona. Solo tiene que pasar los nombres de propiedad como cadenas.
clami219
3
Ah, gracias por aclarar. Pensé que intentabas usar la función de lenguaje get () / set (), no escribir tu propio get () / set (). Sin embargo, todavía no me gusta esta solución porque realmente no resuelve el problema original.
John Kurlak
@JohnKurlak Bueno, escribí que es un enfoque original. Proporciona una forma diferente de resolver el problema, a pesar de que no resuelve el problema donde tiene un código existente que utiliza un enfoque más tradicional. Pero es bueno si estás empezando desde cero. Seguramente no merece un
voto negativo
@JohnKurlak ¡Mira si ahora se ve mejor! :)
clami219
0

Prefacio:

La respuesta de TJ Crowder menciona un Proxy, que será necesario para un captador / configurador general para propiedades que no existen, como lo pedía el OP. Dependiendo de qué comportamiento se quiera realmente con getters / setters dinámicos, a Proxypuede no ser realmente necesario; o, potencialmente, es posible que desee utilizar una combinación de a Proxycon lo que le mostraré a continuación.

(PD: He experimentado con ProxyFirefox en Linux recientemente y he encontrado que es muy capaz, pero también algo confuso / difícil de trabajar y hacerlo bien. Más importante, también he encontrado que es bastante lento (al menos en relación con lo optimizado que tiende a ser JavaScript hoy en día): estoy hablando en el ámbito de los deca-múltiplos más lento).


Para implementar getters y setters creados dinámicamente específicamente, puede usar Object.defineProperty()o Object.defineProperties(). Esto también es bastante rápido.

La esencia es que puede definir un captador y / o definidor en un objeto de la siguiente manera:

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.

Varias cosas a tener en cuenta aquí:

  • No puede usar la valuepropiedad en el descriptor de propiedad ( no se muestra arriba) simultáneamente con gety / o set; de los documentos:

    Los descriptores de propiedad presentes en los objetos tienen dos tipos principales: descriptores de datos y descriptores de acceso. Un descriptor de datos es una propiedad que tiene un valor, que puede ser o no escribible. Un descriptor de acceso es una propiedad descrita por un par de funciones getter-setter. Un descriptor debe ser uno de estos dos sabores; No puede ser ambos.

  • Por lo tanto, notará que creé una valpropiedad fuera del Object.defineProperty()descriptor de llamada / propiedad. Este es un comportamiento estándar.
  • De acuerdo con el error aquí , no ajuste writablea trueen el descriptor de propiedad si se utiliza geto set.
  • Es posible que desee considerar la configuración configurabley enumerable, sin embargo, dependiendo de lo que busca; de los documentos:

    configurable

    • true si y solo si el tipo de este descriptor de propiedad puede cambiarse y si la propiedad puede eliminarse del objeto correspondiente.

    • Por defecto es falso.


    enumerable

    • true si y solo si esta propiedad aparece durante la enumeración de las propiedades en el objeto correspondiente.

    • Por defecto es falso.


En esta nota, estos también pueden ser de interés:

  • Object.getOwnPropertyNames(obj): obtiene todas las propiedades de un objeto, incluso las que no se pueden enumerar (¡ASÍ QUE esta es la única forma de hacerlo!).
  • Object.getOwnPropertyDescriptor(obj, prop): obtiene el descriptor de propiedad de un objeto, el objeto que se pasó Object.defineProperty()arriba.
  • obj.propertyIsEnumerable(prop);: para una propiedad individual en una instancia de objeto específica, llame a esta función en la instancia de objeto para determinar si la propiedad específica es enumerable o no.
Andrés
fuente
2
Me temo que has leído mal la pregunta. El OP solicitó específicamente capturar todo como PHP __gety__set . definePropertyno maneja ese caso. De la pregunta: "Es decir, crear captadores y definidores para cualquier nombre de propiedad que no esté definido". (Su énfasis). definePropertydefine propiedades de antemano. La única forma de hacer lo que solicitó el OP es un proxy.
TJ Crowder
@TJCrowder Ya veo. Es técnicamente correcto, aunque la pregunta no era muy clara. He ajustado mi respuesta en consecuencia. Además, algunos pueden querer una combinación de nuestras respuestas (personalmente lo hago).
Andrew
@Andrew cuando hice esta pregunta en 2011, el caso de uso que tenía en mente era una biblioteca que puede devolver un objeto al que el usuario podría llamar de obj.whateverPropertytal manera que la biblioteca pueda interceptar eso con un captador genérico y recibir el nombre de la propiedad. El usuario intentó acceder. De ahí el requisito de 'captadores y establecedores generales'.
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)

esto funciona para mi

Bruno
fuente
13
Usar Function()así es como usar eval. Simplemente ponga directamente las funciones como parámetros de defineProperty. O, si por alguna razón insiste en crear dinámicamente gety set, a continuación, utilice una función de alto orden que crea la función y la devuelve, comovar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l