Using both `get` and `apply` Proxy traps on plain JS object - javascript

The following code is a simple proxy that logs out the "gets" that were trapped:
var p = new Proxy({}, {
get: function(target, property, receiver) {
console.log("getting: ", property);
return target[property];
}
});
When I coerce this into a String with "hello " + p, I get the following output in the console:
getting: Symbol(Symbol.toPrimitive)
getting: valueOf
getting: toString
getting: Symbol(Symbol.toStringTag)
"hello [object Object]"
Everything is fine so far, but let's do something a little sneaky and proxy a function, but actually still use it as a proxy to our plain object we used in the last example. The reason I want this is because I'd like to be able to capture both gets and applys on this obj.
Notice the return target.obj part - we're really using this to proxy obj still - it's just that we're doing it via fn:
var fn = function(){};
fn.obj = {};
var p = new Proxy(fn, {
get: function(target, property, receiver) {
console.log("getting: ", property);
return target.obj[property];
}
});
Now, I'd have thought this would produce exactly the same output as the last example for "hello " + p, but I was wrong:
getting: Symbol(Symbol.toPrimitive)
getting: valueOf
getting: toString
getting: Symbol(Symbol.toStringTag)
"hello [object Function]"
Notice that it has resulted in a Function string tag rather than an Object one. What's going on here? It's as if toString is being called on fn rather than obj. (Edit: But we can add fn.toString = function(){ return "fn"; } and it doesn't change the output, so maybe it's not fn that is being stringified here?)
If you pop a debugger statement in there, you'll see it's actually returning fn.obj.toString as you'd expect, but for some reason the final output is a function rather than an object (though I'm not entirely sure which function). Thanks for your help!
P.S. I haven't explained the full context of my use case (short version: it's for a DSL, so bending "good practice" is fine), and so suggesting alternative patterns to achieve both get and apply traps on an object (in effect) may not be relevant to my particular case. I'd really just like to understand why the above approach isn't working like I expect it to, but would also like to ensure the question is broad enough to help future readers in a similar situation.

I think I've found the bug. When we return a function, it looks like we need to bind it to target.obj, otherwise it's being bound to some function somewhere. I'm not completely up to scratch on this stuff, but I think it makes sense. So here's the updated, working code:
var fn = function(){};
fn.obj = {};
fn.toString = function(){ return "fn"; }
var p = new Proxy(fn, {
get: function(target, property, receiver) {
console.log("getting: ", property);
let result = target.obj[property];
if(typeof result === 'function') {
result = result.bind(target.obj);
}
return result;
}
});

Related

Unexpected Set trap behavior in ES6 Proxy

let ar = [];
let p = new Proxy(new Map(), {
get: (o, k) => {
ar.push(1)
return Reflect.get(o, k).bind(o)
},
set: (o, k, v) => {
ar.push(2)
return Reflect.set(o, k, v)
}
});
p.set(1, 2)
p.get(1)
console.log(ar) //Outputs [1,1]
I am trying to intercept both set and get operations on a Map object. I am in no way trying to extend/subclass a Map.
In the process of proxying said Map object, I came across this weird unexpected behavior, the set trap isn't being fired in the above code, but instead the get trap gets fired twice!
I further proceeded to log the k(key) values from the get trap in the following way;
//same code above
get: (o, k) => {
console.log(k) //Logs set and then get!
return Reflect.get(o, k).bind(o)
}
//same code below
The behavior I expect is for the array to be [2,1] and console.log(k) at the get trap to actually output the value of the key.
I wish to know why this happens as so, I've gone through a couple problems like this in here related to proxyifying maps, none of them lead to any sensible reasoning as to why this is happening.
My end goal is to fire an event at the set trap. Am I using Proxy for something it is meant to be used? If not, what approach should I take? Should I abandon using a Map to an Object Literal even though it will bring all the cons of using one? ex: no length property, string-only properties, no forced-unique keys etc.
UPDATE: The more and more I dig into this proxified Map, the more issues I keep coming across. It now seems to me as if the ES6 Proxy API treats Maps the same way as it does an ordinary object. The answer by Vill and my digging backs this up. Why do I say this? Read the following;
//same code above
get: (o, k) => {
if(k === 'tray') return ']'
return Reflect.get(o, k).bind(o)
}
//same code below
p.tray //returns ]
The above code shouldn't theoretically succeed, right? It is as if Proxy is using the same logic to intercept object operations when intercepting Maps as well! As;
///--While the following---//
let m = new Map();
m.set('tray', ']')
m.tray //undefined
Vill's answer says that the Proxy identifies Map.prototype.set as first reading set and invoking it as a function next.
Doesn't that mean in the set trap I've written in the original code(on the very top) doesn't intercept the modification/setting a property of the Map and in fact the implicit/native to Map-Map.prototype.set is used instead of the Reflect.set which we grant through the Proxy?
Doesn't all this further enforce the fact that Proxy and Maps don't mix together well? Am I heading down the wrong way? What am I misunderstanding if so? Are Proxies supposed to treat Maps like just any other object?
It is not bug it is feature (joke).
You should understand what exactly proxy's .get and .set do. Get will intercept any reading try of the object. Lets take your example:
p.set(1,2)
p.get(1)
On the first line we: read from object property with name set and then try to invoke it as function
On the second line we read from object property with name get and then try to invoke it as a function.
If you will try this code:
p.val = 5;
then you will try to set 5 to the target with name val and setter will fire.
This how proxy's setter and getter work.
So, to achive desired behavior, check the property name and return the function with some additional implementation. And do not forget to call original function.
Somth like this:
get: (o, k) => {
if (k==='get') return Reflect.get(o, k).bind(o);
if (k==='set') return function(v){Reflect.set(o, k, v)}
}
Hope this helps.
Update:
let obj = {
take: (v) => v
};
let handler = {
get: (target, key) => {
if (key === 'take') {
return function(v) {
console.log(`logger says: function ${key} was called with argument: ${v}`);
}
return target[key];
}
}
};
let proxy = new Proxy(obj, handler);
proxy.take(5);
proxy.take(3);

Javascript: silently fail on undefined assignment

Suppose I am trying to assign a variable in a way that causes an exception
I am going to access a non-existent key of a dictionary:
myObject.property = dictionary['NO_KEY'][0];
Now, because 'NO_KEY' does not exist on dictionary, my program will catch an exception when trying to subscript 0 of undefined - and crash. Is it possible to execute this line above as a no-op so that my script can continue running? I know there is try-catch syntex, but is there a more elegant syntax with ESMA6?
You can use an if condition and statement, Object.hasOwnProperty() or as suggested by #Ryan in operator
if (dictionary.hasOwnProperty("NO_KEY")) {
myObject.property = dictionary["NO_KEY"][0];
}
if ("NO_KEY" in dictionary) {
myObject.property = dictionary["NO_KEY"][0];
}
Object.defineProperty(Object.prototype,
'accessWithSilentFail', {
configurable: false,
enumerable: false,
writable: false,
value: function(key) {
return this[key] ? this[key] : {};
}});
myObject.property = dictionary
.accessWithSilentFail('NO_KEY')
.accessWithSilentFail(0);
That way you get an empty object if at any point the chain fails. You need to get an object so the chain doesn't fail halfway. You can call the function something shorter if you're going to use it a lot.
Although this works, it has many, many limitations, and it changes the Object prototype, which is usually frowned upon. You really should consider just checking for undefined, which is the idiomatic way to do it.
If you ever need to check if the access chain failed, you can use:
function chainFailed(result) {
return Object.keys(result).length === 0;
}
So you could do
myObject.property = dictionary
.accessWithSilentFail('NO_KEY')
.accessWithSilentFail(0);
if (!chainFailed(myObject.property)) {
//keep on
} else {
//handle failure
}
This works as long as your expected return isn't an empty object, on which case chainFailed will always return true. But I'm assuming you really want to fail silently, because if you wanted to handle errors you could just use an exception.
Use ternary operator
myObject.property = dictionary['NO_KEY'] ? dictionary['NO_KEY'][0] : null;
While I believe this is a bad idea that will come back to bite you later, here is a solution for modern browsers using proxies. Yes, you are still checking for the property existence, but it is hidden from your code accessing the dictionary keys.
var dictionary = {a: 42};
dictionary = new Proxy(dictionary, {
get: (target, property) => {
if (target.hasOwnProperty(property)) {
return target[property];
}
return {};
}
});
// Existing properties are passed through unchanged
console.log(dictionary.a);
// Missing properties result in an empty object
console.log(dictionary.b);
// Original test
var lost = dictionary['NO_KEY'][0];
console.log(lost);

How to force toString when logging object in javascript

I wonder if it is posible to get "content here" as a response (for example a log) from this code:
function Obj () {
this.toString = function(){ return "content here" };
}
var obj = new Obj;
console.log(obj);
I know I can force it with String(), toString() and ""+obj, but I want to know if there is a way of forcing it from WITHIN the object.
Your edit adding
I know I can force it with String(), toString() and ""+obj, but I want to know if there is a way of forcing it from WITHIN the object.
...changes the question. The simple answer is "no, you can't do that within the object." In order for the toString on your object to be called, something needs to say "I want the primitive form of this" (or specifically "I want the string form of this"). console.log doesn't do that, it provides richer information than that.
Your putting a toString on your object means that any time it's converted to a string, your function will get called, but it doesn't dictate when that happens. You can also use valueOf. There's more about this in the spec: §9.1 - ToPrimitive, §8.12.8 - [[DefaultValue]] (hint), and §9.8 - ToString.
But adding toString (or valueOf) doesn't let you dictate when it happens; you can't, that's just done by the rules of JavaScript or the calling code doing it (explicitly, or implicitly).
Original Answer:
The simplest way is to use String on it:
console.log(String(obj));
You could add your own method:
console.logString = function(s) {
console.log(String(s));
};
You could probably alter log:
var old = console.log;
console.log = function(s) {
var a = Array.prototype.map.call(arguments, function(a) {
return String(a);
};
return old.apply(console, a);
};
...but I would not recommend it.

Get a more useful value than "[object Object]" for debugging?

Couldn't there a be a environment flag in JavaScript so you could turn on some metadata for objects.
So instead when you are debugging and get:
[object Object]
you would get the variable name and type:
[foo String]
why isn't this possible?
JSON.stringify might be what you are looking for, though it won't give you the name of the variable – JavaScript simply can't do that without 3rd party tools.
The constructor function of your object can be reached by using its constructor property, though there's no guarantee with this as the constructor property is writable.
You might also want to look into the debugger statement.
A bit hacky , but it can help you to find what is your object source :
function Foo()
{}
var toClass = function(a)
{
var _name = a.constructor.toString().match(/^function (\w+)/i); //\w probably should be enhanced
console.log(_name[1])
}
toClass( new Foo()) //Foo
toClass( [1, 2]) //Array
toClass( new Date()) //Date
toClass( {"a":2}) //Object
Aside note : don't override toString just for debugging. toString has its purpose. and should be used as it was meant to be used.
To directly answer your question about just flipping a "global flag" instead of changing your debugging methodology:
Assuming you'd only do this during debugging, you can temporarily override the Object.prototype.toString to return a JSON representation of objects:
Object.prototype.toString = function () { return JSON.stringify(this); };
Then in the browser console:
var obj = { a: 42 };
console.log('My object: ' + obj);
Will give you:
My object: {"a":42}
Even if this answers your question, I don't recommend a global override of a base method because it has the potential to cause catastrophic issues. Try relying on unit tests and breakpoints + debugging as others have suggested in comments.

A function to tap into any methods chain

I like how Ruby's .tap method works. It lets you tap into any method chain without breaking the chain. I lets you operate an object then returns the object so that the methods chain can go on as normal. For example if you have foo = "foobar".upcase.reverse, you can do:
"foo = foobar".upcase.tap{|s| print s}.reverse
It will print the upcased (but not reversed) string and proceed with the reversing and assignment just like the original line.
I would like to have a similar function in JS that would serve a single purpose: log the object to console.
I tried this:
Object.prototype.foo = function() {
console.log(this);
return this;
};
Generally, it works (though it outputs Number objects for numbers rather than their numeric values).
But when i use some jQuery along with this, it breaks jQuery and stops all further code execution on the page.
Errors are like this:
Uncaught TypeError: Object foo has no method 'push'
Uncaught TypeError: Object function () { window.runnerWindow.proxyConsole.log( "foo" ); } has no method 'exec'
Here's a test case: http://jsbin.com/oFUvehAR/2/edit (uncomment the first line to see it break).
So i guess that it's not safe to mess with objects' prototypes.
Then, what is the correct way to do what i want? A function that logs current object to console and returns the object so that the chain can continue. For primitives, it should log their values rather than just objects.
You correctly figured out how a method can be safely added anywhere in a chain, but your adding it to the Object.prototype is intrusive and can break code easily. Looks like jQuery code is the one that breaks for you.
A much safer way is:
Object.defineProperty(Object.prototype, 'foo', {
value : function() { console.log( "foo" ); return this; },
enumerable : false
});
DEMO: http://jsbin.com/oFUvehAR/7/edit
Finally, something generic could look like this:
Object.defineProperty(Object.prototype, 'tap', {
value : function(intercept) {
intercept.call(this);
return this;
},
enumerable : false
});
// Usage:
var x = { a:1 };
x.tap(function(){ console.log(this); });
As for the primitives part of your question, that is a bit trickier. When you call the tap method on a primitive, an Object wrapper is created and the tap method is called on it. The primitive value is still available, via the valueOf() method of that Object wrapper, so you could log it. The tricky part is that you have no way of knowing if the "thing" that you wanted to call the tap method on was initially a primitive or an Object wrapper. Assuming you never want to work with Object wrappers (that is quite reasonable), you could do the better tap method posted below.
Object.defineProperty(Object.prototype, 'tap', {
value : function(intercept) {
var val = (this instanceof Number || this instanceof String || this instanceof Boolean) ? this.valueOf() : this;
intercept(val);
return val;
},
enumerable : false
});
var log = console.log.bind(console);
var x = { a : 1 };
x.tap(log);
2.0.tap(log);
Notice that while in the first version of the tap function, the function passed to it had the useful information in this, in the second version it is mandatory to pass it as a parameter.
If you want a specialized logger for it, you can do something like this:
Object.defineProperty(Object.prototype, 'log', {
value : function(){
return this.tap(console.log.bind(console));
},
enumerable : false,
writable : true /* You want to allow objects to overwrite the log method */
});

Categories

Resources