Is it possible to implement dynamic getters/setters in JavaScript?
Asked Answered
H

5

175

I am aware of how to create getters and setters for properties whose names one already knows, by doing something like this:

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

Now, my question is, is it possible to define sort of catch-all getters and setters like these? I.e., create getters and setters for any property name which isn't already defined.

The concept is possible in PHP using the __get() and __set() magic methods (see the PHP documentation for information on these), so I'm really asking is there a JavaScript equivalent to these?

Needless to say, I'd ideally like a solution that is cross-browser compatible.

Hallvard answered 25/10, 2011 at 15:45 Comment(1)
I managed to do it, see my answer here for how.Alyse
B
273

This changed as of the ES2015 (aka "ES6") specification: JavaScript now has proxies. Proxies let you create objects that are true proxies for (facades on) other objects. Here's a simple example that turns any property values that are strings to all caps on retrieval, and returns "missing" instead of undefined for a property that doesn't exist:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    example: "value",
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        if (Reflect.has(target, name)) {
            let rv = Reflect.get(target, name, receiver);
            if (typeof rv === "string") {
                rv = rv.toUpperCase();
            }
            return rv;
        }
        return "missing";
      }
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`);       // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`);       // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`);       // "proxy.example = UPDATED"

Operations you don't override have their default behavior. In the above, all we override is get, but there's a whole list of operations you can hook into.

In the get handler function's arguments list:

  • target is the object being proxied (original, in our case).
  • name is (of course) the name of the property being retrieved, which is usually a string but could also be a Symbol.
  • receiver is the object that should be used as this in the getter function if the property is an accessor rather than a data property. In the normal case this is the proxy or something that inherits from it, but it can be anything since the trap may be triggered by Reflect.get.

This lets you create an object with the catch-all getter and setter feature you want:

"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.example = ${obj.example}`);
obj.example = "value";
console.log(`[after] obj.example = ${obj.example}`);

The output of the above is:

Getting non-existent property 'example'
[before] obj.example = undefined
Setting non-existent property 'example', initial value: value
[after] obj.example = value

Note how we get the "non-existent" message when we try to retrieve example when it doesn't yet exist, and again when we create it, but not after that.


Answer from 2011 (obsoleted by the above, still relevant to environments limited to ES5 features like Internet Explorer):

No, JavaScript doesn't have a catch-all property feature. The accessor syntax you're using is covered in Section 11.1.5 of the spec, and doesn't offer any wildcard or something like that.

You could, of course, implement a function to do it, but I'm guessing you probably don't want to use f = obj.prop("example"); rather than f = obj.example; and obj.prop("example", value); rather than obj.example = value; (which would be necessary for the function to handle unknown properties).

FWIW, the getter function (I didn't bother with setter logic) would look something like this:

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

But again, I can't imagine you'd really want to do that, because of how it changes how you use the object.

Byway answered 25/10, 2011 at 15:47 Comment(0)
L
19

Preface:

T.J. Crowder's answer mentions a Proxy, which will be needed for a catch-all getter/setter for properties which don't exist, as the OP was asking for. Depending on what behavior is actually wanted with dynamic getters/setters, a Proxy may not actually be necessary though; or, potentially, you may want to use a combination of a Proxy with what I'll show you below.

(P.S. I have experimented with Proxy thoroughly in Firefox on Linux recently and have found it to be very capable, but also somewhat confusing/difficult to work with and get right. More importantly, I have also found it to be quite slow (at least in relation to how optimized JavaScript tends to be nowadays) - I'm talking in the realm of deca-multiples slower.)


To implement dynamically created getters and setters specifically, you can use Object.defineProperty() or Object.defineProperties(). This is also quite fast.

The gist is that you can define a getter and/or setter on an object like so:

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.

Several things to note here:

  • You cannot use the value property in the property descriptor (not shown above) simultaneously with get and/or set; from the docs:

    Property descriptors present in objects come in two main flavors: data descriptors and accessor descriptors. A data descriptor is a property that has a value, which may or may not be writable. An accessor descriptor is a property described by a getter-setter pair of functions. A descriptor must be one of these two flavors; it cannot be both.

  • Thus, you'll note that I created a val property outside of the Object.defineProperty() call/property descriptor. This is standard behavior.
  • As per the error here, don't set writable to true in the property descriptor if you use get or set.
  • You might want to consider setting configurable and enumerable, however, depending on what you're after; from the docs:

    configurable

    • true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.

    • Defaults to false.


    enumerable

    • true if and only if this property shows up during enumeration of the properties on the corresponding object.

    • Defaults to false.


On this note, these may also be of interest:

Lake answered 4/7, 2020 at 8:49 Comment(4)
I'm afraid you've misread the question. The OP specifically asked for catch all like PHP's __get and __set. defineProperty doesn't handle that case. From the question: "I.e., create getters and setters for any property name which isn't already defined." (their emphasis). defineProperty defines properties in advance. The only way to do what the OP asked for is a proxy.Byway
@T.J.Crowder I see. You are technically correct, though the question wasn't very clear. I've adjusted my answer accordingly. Also, some may actually want a combination of our answers (I personally do).Lake
@Lake when I asked this question back in 2011 the use-case I had in mind was a library that can return an object on which the user could call obj.whateverProperty such that the library can intercept that with a generic getter and be given the property name the user tried to access. Hence the requirement for 'catch-all getters and setters'.Hallvard
@Lake your answer was helpful to meEmmuela
A
5

The following could be an original approach to this problem:

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

In order to use it the properties should be passed as strings. So here is an example of how it works:

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

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

Edit: An improved, more object-oriented approach based on what I proposed is the following:

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

You can see it working here.

Arleanarlee answered 30/5, 2014 at 10:23 Comment(10)
This doesn't work. You can't define a getter without specifying the name of the property.Wanigan
@JohnKurlak Check this jsFiddle: jsfiddle.net/oga7ne4x It works. You only have to pass the property names as strings.Arleanarlee
Ah, thanks for clarifying. I thought you were trying to use the get()/set() language feature, not writing your own get()/set(). I still don't like this solution though because it doesn't really solve the original problem.Wanigan
@JohnKurlak Well, I wrote it's an original approach. It provides a different way to solve the problem, even though it doesn't solve the problem where you have an existing code that uses a more traditional approach. But it's good if you are starting from scratch. Surely not worthy a downvote...Arleanarlee
@JohnKurlak See if now it looks better! :)Arleanarlee
What you've posted is more reusable, but every time I've run into the problem of wanting dynamic getters and setters (even when starting from scratch) a solution like this is too much overhead for what I want.Wanigan
@JohnKurlak Well, overhead is what you get when you need solutions which are not provided natively. Whether it's too much or not, is a subjective thing to decide. Thanks for expressing your thought! :)Arleanarlee
I suppose. Usually when I'm looking on StackOverflow, I'd rather quickly see that there is no native way to do what I want than to have to parse through 5 solutions to see what they're actually doing. Either way, I removed the downvote in case someone finds your answer helpful.Wanigan
@JohnKurlak Thanks! :) Anyway it was useful, because this conversation with you encouraged me to improve my answer. So, thanks for that as well! :)Arleanarlee
My answer shows the equivalent of this without the need to explicitly call any functions.Lake
S
4

I was looking for something and I figured out on my own.

/*
    This function takes an object and converts to a proxy object.
    It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {

    return new Proxy(original, {

        get(target, name, receiver) {
            let rv = Reflect.get(target, name, receiver);
            return rv;
        },

        set(target, name, value, receiver) {

            // Proxies new objects 
            if(typeof value === "object"){
                value = getProxy(value);
            }

            return Reflect.set(target, name, value, receiver);
        }
    })
}

let first = {};
let proxy = getProxy(first);

/*
    Here are the tests
*/

proxy.name={}                               // object
proxy.name.first={}                         // nested object
proxy.name.first.names=[]                   // nested array 
proxy.name.first.names[0]={first:"vetri"}   // nested array with an object

/*
    Here are the serialised values
*/
console.log(JSON.stringify(first))  // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy))  // {"name":{"first":{"names":[{"first":"vetri"}]}}}
Serin answered 6/10, 2021 at 4:27 Comment(1)
Given an object, return me an object signals on CRUD on its properties.Serin
W
-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)

this works for me

Welldisposed answered 28/11, 2013 at 20:55 Comment(1)
Using Function() like that is like using eval. Just put directly the functions as parameters of defineProperty. Or, if for some reason you insist to dynamically create get and set, then use a high-order function that creates the function and return it, like var get = (function(propName) { return function() { return this[propName]; };})('value');Haematozoon

© 2022 - 2024 — McMap. All rights reserved.