reflection appears to be losing this - javascript

I'm trying to build an AOP logger for my classes... I'm having an issue where when i reflect back to the targeted function, the function loses access to this
so my AOP kinda looks like this
AOP.js
class AOP {
constructor() {
}
static ClassHandler(obj) {
const InstanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const origMethod = target[prop];
return function (...args) {
// let result = Reflect.apply(origMethod, this, args)
let result = Reflect.get(target, prop, receiver)
result = Reflect.apply(result, this, args);
console.log(prop + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
},
apply(target, thisArg, argumentsList) {
console.log('actually applied');
}
}
const handler = {
construct(target, args) {
console.log(`${target.name} instantiated`);
console.log(args);
const instance = Reflect.construct(...arguments);
return new Proxy(instance, InstanceHandler);
}
}
return new Proxy(obj, handler);
}
}
module.exports = AOP;
A singleton
OtherClass.js
class OtherClass {
constructor() {
this._blah = 'this is a shoutout';
}
shoutOut() {
console.log(this._blah);
}
}
module.exports = new OtherClass();
and a class which requires the singleton
CalculatorDI.js
class Calculator {
constructor(otherClass) {
this.otherClass = otherClass;
}
add(a, b) {
this.otherClass.shoutOut();
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calculator;
bringing it all together like this:
const AOP = require('./src/aspects/AOP');
const Calculator = AOP.ClassHandler(require('./src/CalculatorDI'));
const otherClass = require('./src/OtherClass');
const calculator = new Calculator(otherClass);
calculator.add(1,1);
When running this, I get the error:
TypeError: this.otherClass.shoutOut is not a function

Your problem is that your proxy always returns a function, for any property that is accessed, including this.otherClass. You will need to use
const instanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const orig = Reflect.get(target, prop, receiver);
if (typeof orig == "function") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return function (...args) {
const result = orig.apply(this, args);
console.log(prop + JSON.stringify(args) + ' -> ' + JSON.stringify(result));
return result;
};
} else {
return orig;
}
}
};
Also notice that you don't need an apply trap in the instanceHandler, as none of your instances is a function.

Related

Why does mocha chai should not prove identity for `return this`, if I use Proxy:get in the constructor?

I want to write a class, that deals with undefined properties. I also want to return this to be able to chain methods to create a domain specific language (DSL).
I return a Proxy from the constructor, to handle undefined properties. Now when testing the instance, it does happen, that return this does not prove to be identical with the instance. I fear bugs resulting from this, although I can chain the methods as intended.
This is a mocha chai test to show the behaviour. Replace o.that().should.not.equal(o); with o.that().should.equal(o); in the last instruction to see how it fails.
require('chai').should();
describe('chai testing classes using Proxy', () => {
it('asserts object identity, if an instance method returns this', () => {
const o = new class NormalClass{ }
o.that = function() { return this; }
o.that().should.equal(o);
});
it('observes the same behaviour for constructors returning a dummy Proxy', () => {
const o = new class ProxyClass{
constructor() { return new Proxy(this, {}); }
}
o.that = function() { return this; }
o.that().should.equal(o);
});
it('requires deep.equal on the other hand, if the Proxy handles get', () => {
const o = new class ProxyClassPlusGet{
constructor() {
return new Proxy(this, {
get: function(target, prop) { return target[prop]; },
});
}
}
o.that = function() { return this; }
o.that().should.deep.equal(o);
o.that().should.not.equal(o);
});
});
Your implementation works insofar as o.that() === o yields true.
But it does not work with getters, which interferes with chai's should. You can reproduce this with
const o = new Proxy({
get self() { return this; },
that() { return this; },
}, {
get(target, prop) { return target[prop]; },
});
console.log(o.self === o);
console.log(o.that() === o);
Why is that? Because your get trap is broken, ignoring the receiver of the property access. It will hold the proxy, o, but when you do return target[prop] then target will be the receiver. You can fix it by using Reflect.get:
const o = new Proxy({
get self() { return this; },
that() { return this; },
}, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
// ^^^^^^^^
},
});
console.log(o.self === o);
console.log(o.that() === o);
In addition to the concise answer given by #Bergi I add another test. It proves, that his solution shows a sound behaviour:
It returns the identity of the proxy object in all cases.
It stores the data into the underling original object.
It doesn't create a vicious circle with the proxy as receiver.
It works consistently with chai expect and chai should.
I still don't fully understand the proxying stuff. Doing this tests, already gives a deep insight. Kudos to #Bergi for the right solution.
require('chai').should();
const expect = require('chai').expect;
describe('general Proxy behaviour', () => {
describe('Proxy with empty handler', () => {
it('shows that original and proxy are two different objects' , () => {
const original = {};
const proxy = new Proxy(original, {});
expect(proxy).not.equal(original);
});
it('evaluates proxy and origianl to be deep equal' , () => {
const original = {};
const proxy = new Proxy(original, {});
expect(proxy).to.deep.equal(original);
});
it('sets and gets down to the original', () => {
const original = {};
const proxy = new Proxy(original, {});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('consistent when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, { });
it('evaluates getters to the proxy', () => {
expect(proxy.that).to.be.equal(proxy)
});
it('evaluates methods to the proxy, too', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should works like chai expect', () => {
proxy.that.should.equal(proxy)
proxy.getThat().should.equal(proxy)
});
});
});
describe('Proxy without reflection', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
set: function(target, prop, value) { target[prop]=value; },
get: function(target, prop) { return target[prop]; },
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
it('detects original as the target in get and set',
() => {
const original = {};
let targetInSet;
let targetInGet;
const proxy = new Proxy(original, {
set: function(target, prop, value, receiver) {
targetInSet = target;
target[prop]=value;
},
get: function(target, prop, receiver) {
targetInGet = target;
expect(target).to.equal(original);
return target[prop];
},
});
proxy.x = 1;
proxy.x;
expect(targetInSet).to.equal(original);
expect(targetInGet).to.equal(original);
});
it('detects proxy as the receiver in get and set',
() => {
const original = {};
let receiverInSet;
let receiverInGet;
const proxy = new Proxy(original, {
set: function(target, prop, value, receiver) {
receiverInSet = receiver;
target[prop]=value;
},
get: function(target, prop, receiver) {
receiverInGet = receiver;
return target[prop];
},
});
proxy.x = 1;
proxy.x;
expect(receiverInSet).to.equal(proxy);
expect(receiverInGet).to.equal(proxy);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
receiverInGet = receiver;
return target[prop];
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy without the receiver being set', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop) {
return Reflect.get(target, prop);
},
set: function(target, prop, value) {
Reflect.set(target, prop, value);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop);
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy with the receiver being set to original', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, target);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, target);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
describe('chaos when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, target);
},
});
it('evaluates getters to the original', () => {
expect(proxy.that).to.be.equal(original)
});
it('evaluates methods to the proxy', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should differs from chai expect', () => {
expect(proxy.getThat()).to.be.equal(proxy)
proxy.getThat().should.equal(original)
});
it('chai should evaluates to original in both cases', () => {
proxy.getThat().should.equal(original)
proxy.that.should.equal(original)
});
});
});
describe('Reflect in Proxy with the receiver being set to proxy', () => {
it('sets and gets down to the original like the empty handler', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, receiver);
},
});
proxy.x = 1;
expect(original.x).to.equal(1);
original.x = 2;
expect(proxy.x).to.equal(2);
});
it('does not cause a vicious circle in the proxy', () => {
const original = {};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
Reflect.set(target, prop, value, receiver);
},
});
proxy.x = 1;
expect(proxy.x).to.equal(1);
});
describe('consistent when accessing this', () => {
const original = {
getThat() { return this; },
get that() { return this; },
};
const proxy = new Proxy(original, {
get: function(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
},
});
it('evaluates getters to the proxy', () => {
expect(proxy.that).to.be.equal(proxy)
});
it('evaluates methods to the proxy, too', () => {
expect(proxy.getThat()).to.be.equal(proxy)
});
it('chai should works like chai expect', () => {
proxy.that.should.equal(proxy)
proxy.getThat().should.equal(proxy)
});
});
});
});

Extend Array.push method

I'm trying to extend a specific array with custom push method:
let instance = {
'queue': []
};
instance.initQueue = () => {
let _this = instance;
_this['queue'].push = func => {
if (typeof func === 'function') {
// default
Array.prototype.push.apply(this, arguments);
// process
_this.processQueue();
}
};
_this.processQueue();
};
instance.processQueue = () => {
let _this = instance;
_this['queue'].forEach((func, idx, obj) => {
if (typeof func === 'function') {
func.call(func);
}
obj.splice(idx, 1);
});
};
instance.initQueue();
instance.queue.push(() => console.log(1))
While I'm trying to trigger the push method (instance.queue.push(() => console.log(1));, nothing happen. If I'll wrap the initQueue function with a timeout - it's work:
setTimeout(() => { instance.initQueue(); }, 100);
Any reasonable explanation of why this is happening?
After removing the obvious syntax errors, your code seems to be working:
let instance = {
'queue': []
};
instance.initQueue = () => {
let _this = instance;
_this['queue'].push = (func,...rest) => {
if (typeof func === 'function') {
// default
Array.prototype.push.apply(_this['queue'], [func, ...rest]);
// process
_this.processQueue();
}
};
_this.processQueue();
};
instance.processQueue = () => {
let _this = instance;
_this['queue'].forEach((func, idx, obj) => {
if (typeof func === 'function') {
func.call(func);
}
obj.splice(idx, 1);
});
};
instance.initQueue();
instance.queue.push(() => console.log(1))
The functions are being called as soon as they're pushed in the queue, that's because of:
// process
_this.processQueue();
You can remove it to control the processing of the queue yourself.

mixing constructor and apply traps on the Javascript Proxy object

I have a class that I'd like to apply a proxy to, observing method calls and constructor calls:
Calculator.js
class Calc {
constructor(){}
add(a, b) {
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calc;
index.js
const Calculator = require('./src/Calculator');
const CalculatorLogger = {
construct: function(target, args, newTarget) {
console.log('Object instantiated');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Method called');
}
}
const LoggedCalculator = new Proxy(Calculator, CalculatorLogger);
const calculator = new LoggedCalculator();
console.log(calculator.add(1,2));
When this is called, I would expect for the output to be:
Object instantiated
Method called
however, the apply is not being called, I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
How can i build an all encompassing Proxy to "observe" on method calls and constructor calls.
I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
You are totally right, proxies act upon objects, so it won't call apply unless a function property of the Calculator class is called, as follows:
class Calculator {
constructor() {
this.x = 1;
}
instanceFunction() {
console.log('Instance function called');
}
static staticFun() {
console.log('Static Function called');
}
}
const calcHandler = {
construct(target, args) {
console.log('Calculator constructor called');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Function called');
return target(...argumentsList);
}
};
Calculator = new Proxy(Calculator, calcHandler);
Calculator.staticFun();
const obj = new Calculator();
obj.instanceFunction();
With that clear, what you could do to wrap an instance of Calculator with a proxy could be:
Have the class proxy to proxify instances on construct:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
console.log('Function called');
return target(...args);
}
}
const CalculatorClassHandler = {
construct(target, args) {
const instance = new target(...args);
return new Proxy(instance, CalculatorInstanceHandler);
}
}
Have a factory function in the Calculator class in order to create proxified instances:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
return target(...args);
}
};
class Calculator {
static getNewCalculator() {
const instance = new Calculator();
return new Proxy(instance, CalculatorInstanceHandler);
}
}
Instead of using handler.apply() on the class, modify what handler.construct() returns, adding a Proxy to that instead.
class originalClass {
constructor() {
this.c = 1;
}
add(a, b) {
return a + b + this.c;
}
}
const proxiedClass = new Proxy(originalClass, {
construct(target, args) {
console.log("constructor of originalClass called.");
return new Proxy(new target(...args), {
get(target, prop, receiver) {
console.log(prop + " accessed on an instance of originalClass");
const val = target[prop];
if (typeof target[prop] === "function") {
console.log(prop + " was a function");
return function(...args) {
console.log(prop + "() called");
return val.apply(this, args);
};
} else {
return val;
}
}
});
}
});
const proxiedInstance = new proxiedClass();
console.log(proxiedInstance.add(1, 2));
There's 2 proxies in play here:
A proxy to observe constructor calls, and wrap any instances created by that constructor with...
...a proxy to observe property accesses, and log when those properties are functions. It will also wrap any functions, so it can observe calls to that function.

Chaining methods with javascript

I'm trying to create chaining with the javascript methods similar to what we have with jquery. Please let me know how to implement chaining with javascript.
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user;
return this.currentUser.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId();
As I already explained, since you are returning a string from fnFormatUserName you cannot use it for chaining.
To enable chaining, you need to return the object which invoked method. So, you cannot use getter methods for chaining.
In your example, the way to handle it is to have getter methods and methods with updates the object which can be used for chaining like
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
this.currentUser = user.toUpperCase();
return this;
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
document.body.innerHTML = output;
Another version could be
var controller = {
currentUser: '',
fnFormatUserName: function(user) {
if (arguments.length == 0) {
return this.currentUser;
} else {
this.currentUser = user.toUpperCase();
return this;
}
},
fnCreateUserId: function() {
this.userId = this.currentUser + Math.random();
return this;
},
getUserId: function() {
return this.userId;
}
}
var output = controller.fnFormatUserName('Manju').fnCreateUserId().getUserId();
r1.innerHTML = output;
r2.innerHTML = controller.fnFormatUserName();
<div id="r1"></div>
<div id="r2"></div>
You can use proxies to decorate methods so that they will return the object itself ("this") instead of the actual method return value. Below is an implementation of a chainer function that will do just that for any object. The code also declares a special symbol "target" which can be used to access the original object (and unaltered method return values), discarding the chaining proxy.
const target = Symbol('Symbol for the target of the chainer proxy');
const targetSymbol = target;
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
prop === targetSymbol
? target
: typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return receiver;
},
})
: target[prop],
});
const controller = {
currentUser: '',
fnFormatUserName: function(user) {
return this.currentUser = user.toUpperCase();
},
fnCreateUserId: function() {
return this.currentUser + Math.random();
}
}
const output = chainer(controller).fnFormatUserName('Manju')[target].fnCreateUserId();
console.log(output);
Another option would be that the decorated methods would always return an intermediate object with two properties: the this context ("this") and a reference to the original object with undecorated methods ("target"). See below.
const chainer = (target) =>
new Proxy(target, {
get: (_, prop, receiver) =>
typeof target[prop] === 'function'
? new Proxy(target[prop], {
apply: (f, _, args) => {
f.apply(target, args);
return {
this: receiver,
target,
}
},
})
: target[prop],
});
const counter = {
value: 0,
increment: function() {
return ++this.value;
}
}
const value = chainer(counter)
.increment().this
.increment().target
.increment();
console.log(value);
I suppose this might be seen as "cheating", but you could very easily achieve a similar result by extending the String.prototype like:
String.prototype.upper=function(){return this.toUpperCase()};
String.prototype.makeId=function(){return this+Math.random()};
// test
const str="abc";
console.log(str.upper().makeId(), str);
It will, of course, change the behaviour of all strings in the current session as they will now have the additional methods .upper() and .makeId() associated with them.

Is this a valid recursive function?

I found a recursive expression in a library very confused.
The code is here :
https://github.com/tappleby/redux-batched-subscribe/blob/master/src/index.js#L22
export function batchedSubscribe(batch) {
if (typeof batch !== 'function') {
throw new Error('Expected batch to be a function.');
}
const listeners = [];
function subscribe(listener) {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
};
}
function notifyListenersBatched() {
batch(() => listeners.slice().forEach(listener => listener()));
}
return next => (...args) => {
const store = next(...args);
const subscribeImmediate = store.subscribe;
function dispatch(...dispatchArgs) {
const res = store.dispatch(...dispatchArgs);
notifyListenersBatched();
return res;
}
return {
...store,
dispatch,
subscribe,
subscribeImmediate
};
};
}
Specifically this part:
return next => (...args) => {
const store = next(...args);
const subscribeImmediate = store.subscribe;
function dispatch(...dispatchArgs) {
const res = store.dispatch(...dispatchArgs);
notifyListenersBatched();
return res;
}
return {
...store,
dispatch,
subscribe,
subscribeImmediate
};
};
How is this not infinite recursion?
How is this not infinite recursion?
There is absolutely no recursion here. The syntax next => (...args) => … does not translate to
return function next(...args) {
const store = next(...args);
…
but rather to
return function(next) {
return function(...args) {
const store = next(...args);
…
So unless the caller of that function does something weird like var f = batchedSubscribe(…); f(f)(f)…;, it won't call itself.
The reason we both seemed confused by this is because the arrow function, if written as a single statement, implicitly calls return.
So for example a simple function like this:
const add = (a, b) => a + b;
is equivalent to
var add = function(a, b) {
return a + b;
}
Knowing this we can remove the sugar and convert the arrow functions:
return next => function(...args) { // body }
Is really what's going on here, and if we go one step further we get this:
return function(next) {
return function(...args) {
const store = next(...args);
const subscribeImmediate = store.subscribe;
function dispatch(...dispatchArgs) {
const res = store.dispatch(...dispatchArgs);
notifyListenersBatched();
return res;
}
return {
...store,
dispatch,
subscribe,
subscribeImmediate
};
}
}
Both functions that are containing the code are actually nameless. next is a function, but not one of the functions being returned. It is passed as a variable into the first returned function.
There's no recursion here, but rather a lot of function composition, which is to be expected from a library like redux that draws so much from functional programming.

Categories

Resources