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


Soy consciente de cómo crear getters y setters para propiedades cuyos nombres uno ya conoce, 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 fijadores generales como estos? Es decir, crear getters y setters para cualquier nombre de propiedad que no esté ya definido.

El concepto es posible en PHP usando los métodos __get() y __set() magic (ver la documentación de PHP para obtener información sobre estos), así que estoy realmente preguntando ¿hay un JavaScript equivalente a estos?

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

Author: Roman C, 2011-10-25

4 answers

2013 y Actualización de 2015 (vea a continuación la respuesta original de 2011):

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

var original = {
    "foo": "bar"
};
var proxy = new Proxy(original, {
    get: function(target, name, receiver) {
        var rv = target[name];
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log("original.foo = " + original.foo); // "bar"
console.log("proxy.foo = " + proxy.foo);       // "BAR"

"use strict";
(function() {
    if (typeof Proxy == "undefined") {
        console.log("This browser doesn't support Proxy");
        return;
    }
    var original = {
        "foo": "bar"
    };
    var proxy = new Proxy(original, {
        get: function(target, name, receiver) {
            var rv = target[name];
            if (typeof rv === "string") {
                rv = rv.toUpperCase();
            }
            return rv;
        }
    });
    console.log("original.foo = " + original.foo); // "bar"
    console.log("proxy.foo = " + proxy.foo);       // "BAR"
})();

Las operaciones que no se anulan tienen su comportamiento predeterminado. En lo anterior, todo lo que sobrescribimos es get, pero hay una lista completa de operaciones a las que puedes conectarte.

En la lista de argumentos de la función manejadora get:

  • target es el objeto que está siendo proxy (original, en nuestro caso).
  • name es (por supuesto) el nombre de la propiedad que se está recuperando.
  • receiver es el proxy en sí o algo que hereda de él. En nuestro caso, receiver es === proxy, pero si proxy fueron utilizados como un prototipo, receiver podría ser un objeto descendiente, por lo tanto está en la firma de la función (pero al final, por lo que puede dejarlo fácilmente si, como en nuestro ejemplo anterior, no lo usa realmente).

Esto le permite crear un objeto con la característica getter y setter que desee:

var obj = new Proxy({}, {
    get: function(target, name) {
        if (!(name in target)) {
            console.log("Getting non-existant property '" + name + "'");
            return undefined;
        }
        return target[name];
    },
    set: function(target, name, value) {
        if (!(name in target)) {
            console.log("Setting non-existant property '" + name + "', initial value: " + value);
        }
        target[name] = value;
        return true;
    }
});

console.log("[before] obj.foo = " + obj.foo);
obj.foo = "bar";
console.log("[after] obj.foo = " + obj.foo);

"use strict";
(function() {
    if (typeof Proxy == "undefined") {
        console.log("This browser doesn't support Proxy");
        return;
    }

    var obj = new Proxy({}, {
        get: function(target, name) {
            if (!(name in target)) {
                console.log("Getting non-existant property '" + name + "'");
                return undefined;
            }
            return target[name];
        },
        set: function(target, name, value) {
            if (!(name in target)) {
                console.log("Setting non-existant property '" + name + "', initial value: " + value);
            }
            target[name] = value;
            return true;
        }
    });

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

(Tenga en cuenta cómo he dejado receiver fuera de las funciones, ya que no lo usamos. receiver es un cuarto arg opcional en set.)

La salida de lo anterior is:

Getting non-existant property 'foo'
[before] obj.foo = undefined
Setting non-existant property 'foo', initial value: bar
[after] obj.foo = bar

Tenga en cuenta cómo obtenemos el mensaje "non-existant" cuando tratamos de recuperar foo cuando todavía no existe, y de nuevo cuando lo creamos, pero no posteriormente.


Respuesta de 2011 (véase más arriba las actualizaciones de 2013 y 2015):

No, JavaScript no tiene una característica de propiedad catch-all. La sintaxis del accesor que estás usando está cubierta en Sección 11.1.5 de la especificación, y no ofrece ningún comodín o algo así que.

Podría, por supuesto, implementar una función para hacerlo, pero supongo que probablemente no desee usar f = obj.prop("foo"); en lugar de f = obj.foo; y obj.prop("foo", value); en 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 setter) se vería algo como esto:

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 de nuevo, no puedo imaginar que realmente quieras hacer eso, debido a cómo cambia la forma en que usas el objeto.

 150
Author: T.J. Crowder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-13 07:55:13

En Javascript moderno (FF4+, IE9+, Chrome 5+, Safari 5.1+, Opera 11.60+), existe Object.defineProperty. Este ejemplo en el MDN explica muy bien cómo funciona defineProperty, y hace posibles los getters y setters dinámicos.

Técnicamente, esto no funcionará en cualquier consulta dinámica como estás buscando, pero si tus getters y setters válidos están definidos por, digamos, una llamada AJAX a un servidor JSON-RPC, por ejemplo, entonces podrías usar esto de la siguiente manera:

arrayOfNewProperties.forEach(function(property) {
    Object.defineProperty(myObject, property.name, {
        set: property.setter, get: property.getter
    });
});
 37
Author: David Ellis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-13 20:54:18

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. Así que 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, 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 funcionando aquí.

 4
Author: clami219,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-11 09:14:16
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 mí

 -4
Author: Bruno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-11-28 20:55:21