É possível implementar getters / setters dinâmicos em JavaScript?

132

Estou ciente de como criar getters e setters para propriedades cujos nomes já se conhecem, fazendo algo assim:

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

Agora, minha pergunta é: é possível definir tipos de get-get getters e setters como esses? Ou seja, crie getters e setters para qualquer nome de propriedade que ainda não esteja definido.

O conceito é possível no PHP usando os métodos __get()e __set()magic (consulte a documentação do PHP para obter informações sobre estes), então estou realmente perguntando se existe um JavaScript equivalente a esses?

Escusado será dizer que, idealmente, gostaria de uma solução compatível com vários navegadores.

daiscog
fonte
Eu consegui fazer isso, veja minha resposta aqui para saber como.

Respostas:

216

Atualização de 2013 e 2015 (veja abaixo a resposta original de 2011) :

Isso foi alterado conforme a especificação do ES2015 (também conhecida como "ES6"): o JavaScript agora possui proxies . Os proxies permitem criar objetos que são proxies verdadeiros para (fachadas) outros objetos. Aqui está um exemplo simples que transforma todos os valores de propriedade que são cadeias de caracteres em todos os limites na recuperação:

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

As operações que você não substitui têm seu comportamento padrão. Acima, tudo o que substituímos é get, mas há uma lista completa de operações nas quais você pode se conectar.

Na getlista de argumentos da função de manipulador:

  • targeté o objeto que está sendo procurado ( original, no nosso caso).
  • name é (é claro) o nome da propriedade que está sendo recuperada, que geralmente é uma string, mas também pode ser um símbolo.
  • receiveré o objeto que deve ser usado como thisna função getter se a propriedade for um acessador e não uma propriedade de dados. No caso normal, esse é o proxy ou algo que herda dele, mas pode ser qualquer coisa, já que a armadilha pode ser acionada Reflect.get.

Isso permite que você crie um objeto com o recurso catch-all getter e setter que você deseja:

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

A saída do acima é:

Obtendo a propriedade inexistente 'foo'
[antes] obj.foo = indefinido
Configurando a propriedade inexistente 'foo', valor inicial: bar
[depois] obj.foo = bar

Observe como recebemos a mensagem "inexistente" quando tentamos recuperar fooquando ela ainda não existe, e novamente quando a criamos, mas não depois disso.


Resposta de 2011 (veja acima para atualizações de 2013 e 2015) :

Não, o JavaScript não possui um recurso de propriedade abrangente. A sintaxe do acessador que você está usando é abordada na Seção 11.1.5 da especificação e não oferece nenhum curinga ou algo parecido.

Você poderia, é claro, implementar uma função para fazê-lo, mas acho que você provavelmente não deseja usar em f = obj.prop("foo");vez de f = obj.foo;e em obj.prop("foo", value);vez de obj.foo = value;(o que seria necessário para a função manipular propriedades desconhecidas).

FWIW, a função getter (eu não me incomodei com a lógica do setter) ficaria assim:

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

Mas, novamente, não consigo imaginar que você realmente queira fazer isso, por causa de como isso muda a maneira como você usa o objeto.

TJ Crowder
fonte
1
Há uma alternativa para Proxy: Object.defineProperty(). Coloquei os detalhes na minha nova resposta .
Andrew
@ Andrew - eu tenho medo que você tenha interpretado mal a pergunta, veja o meu comentário na sua resposta.
TJ Crowder
4

O seguinte pode ser uma abordagem original para esse 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 usá-lo, as propriedades devem ser passadas como strings. Então, aqui está um exemplo de como funciona:

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

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

Edit: Uma abordagem aprimorada, mais orientada a objetos, com base no que propus é o seguinte:

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

Você pode vê-lo trabalhando aqui .

clami219
fonte
Isso não funciona. Você não pode definir um getter sem especificar o nome da propriedade.
John Kurlak
@JohnKurlak Verifique isso jsFiddle: jsfiddle.net/oga7ne4x Funciona. Você só precisa passar os nomes das propriedades como cadeias.
Clami219
3
Ah, obrigado por esclarecer. Eu pensei que você estava tentando usar o recurso de linguagem get () / set (), não escrevendo seu próprio get () / set (). Ainda não gosto dessa solução, porque ela realmente não resolve o problema original.
John Kurlak
@JohnKurlak Bem, eu escrevi que é uma abordagem original. Ele fornece uma maneira diferente de resolver o problema, mesmo que ele não resolva o problema em que você possui um código existente que usa uma abordagem mais tradicional. Mas é bom se você está começando do zero. Certamente não é digno de um downvote ...
clami219
@JohnKurlak Veja se agora está melhor! :)
clami219
0

Prefácio:

A resposta de TJ Crowder menciona a Proxy, que será necessária para um get-all getter / setter para propriedades que não existem, como o OP estava pedindo. Dependendo de qual comportamento é realmente desejado com getters / setters dinâmicos, um Proxypode não ser realmente necessário; ou, potencialmente, convém usar uma combinação de a Proxycom o que mostrarei abaixo.

(PS: Eu experimentei Proxyminuciosamente no Firefox no Linux recentemente e achei muito capaz, mas também um pouco confuso / difícil de trabalhar e acertar. Mais importante, também achei bastante lento (pelo menos em relação a como o JavaScript otimizado tende a ser hoje em dia) - Estou falando no reino dos deca-múltiplos mais lentamente.)


Para implementar especificamente getters e setters criados dinamicamente, você pode usar Object.defineProperty()or Object.defineProperties(). Isso também é bastante rápido.

A essência é que você pode definir um getter e / ou setter em um objeto como este:

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.

Várias coisas a serem observadas aqui:

  • Você não pode usar a valuepropriedade no descritor de propriedades ( não mostrado acima) simultaneamente com gete / ou set; dos documentos:

    Os descritores de propriedades presentes nos objetos têm dois tipos principais: descritores de dados e descritores de acessadores. Um descritor de dados é uma propriedade que possui um valor que pode ou não ser gravável. Um descritor de acessador é uma propriedade descrita por um par de funções getter-setter. Um descritor deve ser um desses dois tipos; não pode ser ambos.

  • Assim, você notará que eu criei uma valpropriedade fora do Object.defineProperty()descritor de chamada / propriedade. Esse é o comportamento padrão.
  • Conforme o erro aqui , não defina writablea trueno descritor de propriedade, se você usar getou set.
  • Você pode considerar a configuração configurablee enumerable, no entanto, dependendo do que procura; dos documentos:

    configurável

    • true se e somente se o tipo desse descritor de propriedades puder ser alterado e se a propriedade puder ser excluída do objeto correspondente.

    • O padrão é false.


    enumerável

    • true se e somente se essa propriedade aparecer durante a enumeração das propriedades no objeto correspondente.

    • O padrão é false.


Nesta nota, também podem ser de interesse:

  • Object.getOwnPropertyNames(obj): obtém todas as propriedades de um objeto, mesmo as não enumeráveis ​​(AFAIK é a única maneira de fazer isso!).
  • Object.getOwnPropertyDescriptor(obj, prop): obtém o descritor de propriedades de um objeto, o objeto que foi passado Object.defineProperty()acima.
  • obj.propertyIsEnumerable(prop);: para uma propriedade individual em uma instância de objeto específica, chame esta função na instância de objeto para determinar se a propriedade específica é enumerável ou não.
Andrew
fonte
2
Receio que você tenha interpretado mal a pergunta. O OP pediu especificamente capturar todos como PHP __gete__set . definePropertynão lida com esse caso. Da pergunta: "Ou seja, crie getters e setters para qualquer nome de propriedade que ainda não esteja definido." (ênfase deles). definePropertydefine propriedades com antecedência. A única maneira de fazer o que o OP pediu é um proxy.
TJ Crowder
@TJCrowder eu vejo. Você está tecnicamente correto, embora a pergunta não tenha sido muito clara. Eu ajustei minha resposta de acordo. Além disso, alguns podem realmente querer uma combinação de nossas respostas (eu pessoalmente).
Andrew
@ Andrew, quando fiz essa pergunta em 2011, o caso de uso que eu tinha em mente era uma biblioteca que pode retornar um objeto no qual o usuário poderia chamar obj.whateverProperty, de forma que a biblioteca possa interceptar isso com um getter genérico e receber o nome da propriedade usuário tentou acessar. Daí a exigência de 'get-all getters and setters'.
daiscog 6/07
-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)

isso funciona para mim

Bruno
fonte
13
Usar Function()assim é como usar eval. Basta colocar diretamente as funções como parâmetros de defineProperty. Ou, se por algum motivo você insistir em criar gete dinamicamenteset , em seguida, usar uma função de alta ordem que cria a função e devolvê-lo, comovar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l