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;
}
Randy Hall
  • 7,716
  • 16
  • 73
  • 151

5 Answers5

35

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
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    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. – Randy Hall Nov 22 '13 at 14:28
  • @RandyHall: Sadly, in this case, I'm fairly certain no voodoo is available. :-) See the previous answer (originally from 2011) linked above. – T.J. Crowder Nov 22 '13 at 14:36
  • Since you seem to get where I'm going with this... Javascript attempts to find a property, and if it can't be found, attempts to find that property in prototype (from my understanding) - could something along the lines of Foo.prototype.prototype work? Can I catch javascript sniffing through the prototype layer and react to that? – Randy Hall Nov 22 '13 at 14:39
  • 2
    @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. :-) – T.J. Crowder Nov 22 '13 at 14:40
  • @RandyHall You can have a look at Google Dart which compiles to JS and supports proxies in a cross-browser way or use an approach like described in my answer. However the approach I describe implies a change in design and has a performance impact. – plalx Nov 22 '13 at 15:28
  • @plalx: That's interesting, can you point me at a Dart example allowing you to catch access to an undefined property? – T.J. Crowder Nov 22 '13 at 15:56
  • @plalx I think in dart it scans all code and see if undefined properties are requested, if there are it'll create those properties. Kind of like the reverse of removing dead code in Google Closure Compiler but then in reverse. So no JS magic there, just Dart knowing what properties to add to the compiled code based on the full project. – HMR Nov 22 '13 at 16:48
  • @HMR I haven't read about implementation details, however Dart has a `noSuchMethod` concept that can be utilized with the `@proxy` annotation (to avoid warnings). Have a look at http://blog.dartwatch.com/2012/04/playing-with-nosuchmethod-raises-some.html – plalx Nov 22 '13 at 17:28
  • @plalx I would be curious how that is translated to JS but think it's done in the way I described. The compiler scans all the code and any non defined property will point to noSuchmethod. Since all code is already there when compiling Dart knows what properties exist and what properties don't so `myInstance.bananas` is likely compiled to `myInstance.noSuchMethod();` I'll check it out tomorrow; that is if nobody beat me to it. – HMR Nov 22 '13 at 17:38
  • @HMR I do not see any other way of implementing this and it's probably the most efficient way of doing it performance-wise so I guess this is it. – plalx Nov 22 '13 at 17:48
  • Thanks for the edit @LawrenceCherone! – T.J. Crowder Sep 24 '19 at 06:52
  • Is it possible to listen to chained properties with proxy? I mean not just ob.prop, but also obj.prop1.prop2.prop3? to listen whole prop1.prop2.prop3? – Tornike Shavishvili Jun 11 '22 at 21:47
  • 1
    @TornikeShavishvili - Unfortunately, to do that you need a chain of proxies. `obj`, `obj.prop1`, and `obj.prop1.propt2` would all have to be proxies. – T.J. Crowder Jun 12 '22 at 08:29
  • @T.J.Crowder This would be nice feature. Is it possible to submit feature request or something like that? Where can we do it? – Tornike Shavishvili Jun 12 '22 at 09:57
  • 1
    @TornikeShavishvili - In theory you could write a proposal for TC39 (https://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. :-) – T.J. Crowder Jun 12 '22 at 10:02
  • @T.J.Crowder I have same filling :( – Tornike Shavishvili Jun 12 '22 at 10:05
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.

Katana314
  • 8,429
  • 2
  • 28
  • 36
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;
plalx
  • 42,889
  • 6
  • 74
  • 90
  • I'm highly intrigued. Can you add a small example usage to this? – Randy Hall Nov 22 '13 at 15:58
  • @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. – plalx Nov 22 '13 at 17:19
  • Ah alright I see what you're doing there. – Randy Hall Nov 22 '13 at 17:45
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
megawac
  • 10,953
  • 5
  • 40
  • 61
  • 2
    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. – Randy Hall Nov 22 '13 at 14:31
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
Tibos
  • 27,507
  • 4
  • 50
  • 64