27

Is it possible to capture when a (any) property of an object is accessed, or attempting to be accessed?

Example:

I have created custom object Foo

var Foo = (function(){
    var self = {};
    //... set a few properties
    return self;
})();

Then there is some action against Foo - someone tries to access property bar

Foo.bar

Is there way (prototype, perhaps) to capture this? bar may be undefined on Foo. I could suffice with capturing any attempted access to undefined properties.

For instance, if bar is undefined on Foo, and Foo.bar is attempted, something like:

Foo.prototype.undefined = function(){
    var name = this.name; //name of property they attempted to access (bar)
    Foo[name] = function(){
        //...do something
    };
    return Foo[name];
}

But functional, unlike my example.

Concept

Foo.* = function(){
}

Background

If I have a custom function, I can listen for every time this function is called (see below). Just wondering if it's possible with property access.

Foo = function(){};
Foo.prototype.call = function(thisArg){
    console.log(this, thisArg);
    return this;
}
5
  • 2
    Not unless you create getters and setters for the properties and "hide" the real properties Commented Nov 22, 2013 at 14:21
  • @megawac can you post a short example as an answer? I feel I don't need much, just a push in the right direction. Commented Nov 22, 2013 at 14:22
  • Pretty sure it can't be done Commented Nov 22, 2013 at 14:23
  • @RandyHall: He means you would have to change all property accesses to method calls EVERYWHERE in your code. myObj.getFoo(), not myObj.foo and myObj.setFoo(val) Commented Nov 22, 2013 at 14:25
  • @megawac: And that wouldn't work for properties that don't exist. Commented Nov 22, 2013 at 14:32

5 Answers 5

36

Yes, this is possible in ES2015+, using the Proxy. It's not possible in ES5 and earlier, not even with polyfills.

It took me a while, but I finally found my previous answer to this question. See that answer for all the details on proxies and such.

Here's the proxy example from that answer:

const obj = new Proxy({}, {
    get: function(target, name, receiver) {
        if (!(name in target)) {
            console.log("Getting non-existant property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set: function(target, name, value, receiver) {
        if (!(name in target)) {
            console.log("Setting non-existant 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);
obj.foo = "baz";
console.log("[after] obj.foo = " + obj.foo);

Live Copy:

"use strict";

const obj = new Proxy({}, {
    get: function(target, name, receiver) {
        if (!(name in target)) {
            console.log("Getting non-existant property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set: function(target, name, value, receiver) {
        if (!(name in target)) {
            console.log("Setting non-existant 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);
obj.foo = "baz";
console.log("[after] obj.foo = " + obj.foo);

When run, that outputs:

Getting non-existant property 'foo'
[before] obj.foo = undefined
Setting non-existant property 'foo', initial value: bar
[after] obj.foo = bar
[after] obj.foo = baz
Sign up to request clarification or add additional context in comments.

16 Comments

I'm inclined to agree, and this is good information, but I'm going to hold out for a little while before accepting this. Sometimes I'm surprised with the black magic voodoo people come up with.
@RandyHall: Sadly, in this case, I'm fairly certain no voodoo is available. :-) See the previous answer (originally from 2011) linked above.
@RandyHall: Sadly, no. It's simply not possible to intercept that operation today (except on Firefox). This is why proxies are being added to the next edition of the standard. :-)
@TornikeShavishvili - Unfortunately, to do that you need a chain of proxies. obj, obj.prop1, and obj.prop1.propt2 would all have to be proxies.
@TornikeShavishvili - In theory you could write a proposal for TC39 (github.com/tc39/proposals), but beware that the bar for that is very high, and I don't think it's likely something like this would be accepted into the process (but that's just my view). Remember that obj.prop1.prop2.prop3 is multiple expressions, and each is handled on its own (obj.prop1, then doing .prop2 on the result of that, then doing .prop3 on the result of that, ...). I also think the utility of it is quite limited, so it's unlikely to outcompete other ideas for resources. Again, just my view. :-)
|
5

I'll write this under the assumption you're trying to debug something. As Crowder said, this is only available on newer browsers; so it's very useful for testing code that does something you don't want it to. But, I remove it for production code.

Object.defineProperty(Foo, 'bar', {
  set: function() {
    debugger; // Here is where I'll take a look in the developer console, figure out what's
    // gone wrong, and then remove this whole block.
  }
});

Looks like megawac beat me to it. You can also find some Mozilla documentation on the features here.

Comments

1

Like answered already, it will only be possible using the Proxy object in ECMAScript6. Meanwhile, depending on your needs and overall design, you can still achieve this by implementing something similar.

E.g.

function WrappingProxy(object, noSuchMember) {
    if (!this instanceof WrappingProxy) return new WrappingProxy(object);

    this._object = object;

    if (noSuchMember) this.noSuchMember = noSuchMember;
}

WrappingProxy.prototype = {
    constructor: WrappingProxy,

    get: function (propertyName) {
        var obj = this._object;

        if (propertyName in obj) return obj[propertyName];

        if (this.noSuchMember) this.noSuchMember(propertyName, 'property');
    },

    set: function (propertyName, value) {
        return this._object[propertyName] = value;
    },

    invoke: function (functionName) {
        var obj = this._object, 
            args = Array.prototype.slice.call(arguments, 1);

        if (functionName in obj) return obj[functionName].apply(obj, args);

        if (this.noSuchMember) {
            this.noSuchMember.apply(obj, [functionName, 'function'].concat(args));
        }
    },

    object: function() { return this._object },

    noSuchMember: null
};

var obj = new WrappingProxy({
        testProp: 'test',
        testFunc: function (v) {
            return v;
        }
    },
    //noSuchMember handler
    function (name, type) {
        console.log(name, type, arguments[2]);
    }
);

obj.get('testProp'); //test
obj.get('nonExistingProperty'); //undefined, call noSuchMember
obj.invoke('testFunc', 'test'); //test
obj.invoke('nonExistingFunction', 'test'); //undefined, call noSuchMember

//accesing properties directly on the wrapped object is not monitored
obj.object().nonExistingProperty;

3 Comments

I'm highly intrigued. Can you add a small example usage to this?
@RandyHall Did you have a look at the very end of the code? There's a usage example. Basically you wrap any object into a WrappingProxy and then perform all interactions through method calls (including property access). Basically you would have to avoid using non-wrapped version of your objects if you want this to be consistent. If you have a very specific use case for catching non-existing properties it might be possible to use such an approach.
Ah alright I see what you're doing there.
0

With the new defineProperties, defineGetter and defineSetter being added to javascript, you can do something somewhat similar. There is still no true way to hide the __properties__ of an object however. I suggest you see this article.

var obj = {
    __properties__: {
        a: 4
    }
}
Object.defineProperties(obj, {
    "b": { get: function () { return this.__properties__.a + 1; } },
    "c": { get: function (x) { this.__properties__.a = x / 2; } }
});

obj.b // 2
obj.c // .5

This is the classic sort of model that should work in any environment

//lame example of a model
var Model = function(a) {
    this.__properties__ = {a: a};
}

Model.prototype.get = function(item) {
    //do processing
    return this.__properties__[item];
}

Model.prototype.set = function(item, val) {
    //do processing
    this.__properties__[item] = val;
    return this;
}

var model = new Model(5);
model.get("a") // => 5

1 Comment

Good information, but not quite accomplishing what I'm shooting for. I'm trying to prevent "undefined" by defining every property as it's accessed. I want to call a function every time someone tries to access an undefined property of the object, using a function inside the object itself.
0

As the other answers mentioned, at the moment there is no way to intercept undefined properties.

Would this be acceptable though?

var myObj = (function() {
  var props = {
    foo : 'foo'
  }
  return {
    getProp : function(propName) { return (propName in props) ? props[propName] : 'Nuh-uh!' }
  }
}());

console.log(myObj.getProp('foo')); // foo
console.log(myObj.getProp('bar')); // Nuh-uh

1 Comment

Your idea works nicely for custom forms in React.js, thanks!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.