ES6 Proxy Function Call Trap

# Edwin Reynoso (5 years ago)

Let's say we have the following code:

class Person {
	constructor(fName, lName) {
		this.firstName = fName;
		this.lastName = lName;
	}

	logFullName() {
		console.log(this.firstName, this.lastName);
	}
}

var people = new Proxy([new Person('Edwin', 'Reynoso'), new Person('That', 'Guy')], {
	get(target, property) {
		if(target[property]) return target[property];
		var arr = [];
		for(var person of target) arr.push(person[property]);
		return arr;
	}
});

people.firstName // returns ["Edwin", "That"]
people.lastName // returns ["Reynoso", "Guy"]

people.logFullName // returns [Person.prototype.logFullName, Person.prototype.logFullName]

people.forEach // returns forEach meaning any property of the acutal array is returned

I can't call logFullName unless it was a function of the target which is not (and not what I want bc I'll have to do that for every method)

Also I want the function to be called on each element if it could be called on it, if not return undefined

So how about a call trap:

var people = new Proxy([new Person('Edwin', 'Reynoso'), new Person('That', 'Guy')], {
	call(target, functionName, arguments) {
		// target would be the array
		// functionName the name of the function
		// arguments the arguments passed in as an array
	}
});

If the target has a method the handler.get of the proxy above would return that method and therefore you can call it. But what if no such method exist nosuchmethod would be the property yet how would we get the arguments?

Another thing that could possibly done is whenever the property is nosuchmethod pass another argument which would be the actual arguments

Or does someone know how to do what I'm trying to accomplish

# Claude Pache (5 years ago)

Le 9 juin 2015 à 07:58, Edwin Reynoso <eorroe at gmail.com> a écrit :

Or does someone know how to do what I'm trying to accomplish

Maybe the following code goes in the direction of what you want?

var people = new Proxy([new Person('Edwin', 'Reynoso'), new Person('That', 'Guy')], {
	get(target, property) {
		if (target[property] !== undefined) 
			return target[property];
		var arr = [];
		for (var person of target) arr.push(person[property]);
		return new Proxy(arr, { 
			apply(target, thisArg, args) { 
				return target.map(item => Reflect.apply(item, thisArg, args)) 
			} 
		})
	}
});

About your proposal, I recall that, at some point in the history of ES6 draft, there was an [[Invoke]] MOP, and a corresponding invoke trap for proxies, that were finally eliminated.

# Edwin Reynoso (5 years ago)

Alright first I'll say I'm using FF to test because FF supports proxies, and I really don't know how to use Reflect and how it works yet. FF as well does not support Reflect right now. Therefore I'll look into that.

So from this source it seems that it would only work if the actual proxy is invoked as a function?

Take a look at the example in that link as well.

Now I'm not sure if Reflect changes that somehow.

BTW When I ran the code, I didn't get an Error saying that Reflect is not defined I got:

TypeError: people.logFullName is not a function

So I don't think that worked.

# Jason Orendorff (5 years ago)

On Tue, Jun 9, 2015 at 3:44 AM, Edwin Reynoso <eorroe at gmail.com> wrote:

Alright first I'll say I'm using FF to test because FF supports proxies, and I really don't know how to use Reflect and how it works yet. FF as well does not support Reflect right now. Therefore I'll look into that.

That's right. Reflect is a key tool when you're writing proxy handlers. I'm working on implementing it in Firefox now. If you like, you can subscribe to this bug and get updates (quiet so far): bugzilla.mozilla.org/show_bug.cgi?id=987514

Here, I think the technique you're looking for might be that the proxy handler's get() method should return a new proxy (representing an array of methods). This new "method proxy" needs something callable as its target. When the method proxy's .call() trap is called, it'll receive the arguments.

# Tom Van Cutsem (5 years ago)

Edwin,

The "call" trap you suggest has been suggested numerous times before (by the name of the "invoke" trap) and was nearly added to the spec at one point, but eventually removed because of a number of reasons, the more important one being that it breaks the equivalence that a method call in JS is nothing but a property access followed by a function call.

The suggestion made by Claude has nothing to do with Reflect, and you don't necessarily need Reflect to implement it: |Reflect.apply(fun, thisArg, args)| is equivalent to |Function.prototype.apply.call(fun, thisArg, args)|, it's just shorter and more idiomatic.

The important thing is to return, from your get trap, another Proxy that implements the apply trap to be able to intercept the subsequent method call.

Cheers, Tom

2015-06-09 17:25 GMT+02:00 Jason Orendorff <jason.orendorff at gmail.com>:

# Edwin Reynoso (5 years ago)

Ok so I got it work with the following code:

function Person(fName, lName, age) {
	this.firstName = fName;
	this.lastName  = lName;
	this.age       = age;
}

Person.prototype = {
	logFullName() {
		console.log(this.firstName, this.lastName);
	},

	getAge() {
		return this.age;
	}
}

var people = new Proxy([new Person('Edwin', 'Reynoso', 16), new Person('That', 'Guy', 1000)], {
	get(target, property) {
		if(target[property] !== undefined) return target[property];
		var arr = [];
		for(var person of target) {
			if(person[property].constructor === Function) {
				return function(...args) {
					for(var person of target) {
						if(person[property]) arr.push(person[property](...args));
					}
					return arr;
				}
			} else if(person[property]) {
				if(person[property]) arr.push(person[property]);
			} else {
				throw Error(property + ' is not defined');
			}
		}
		return arr;
	}
});

However I still believe there should be an invoke trap in ES6 proxies.

First here's what it would look like:

var people = new Proxy([new Person('Edwin', 'Reynoso', 16), new Person('That', 'Guy', 1000)], {
	get(target, property) {
		if(target[property] !== undefined) return target[property];
		var arr = [];
		for(var person of target) {
			if(person[property]) arr.push(person[property]);
		}
		return arr;
	},
	invoke(target, functionName, arguments) {
		if(target[functionName]) target[functionName](...arguments);
		var arr = [];
		for(var person of target) {
			if(person[functionName]) arr.push(person[functionName](...args));
		}
		return arr;
	}
});

Now isn't that much easier to read and write?

Why have the handler.get trap do all the work.

IMHO It's almost like saying only have one event listener for each type("click") for each element:

document.body.onclick = function() {
	// handle first thing
	// handle second thing
	// handle third thing
}

The above could get really complicated

Over having the following:

document.body.addEventListener('click', function() {
	// handle first thing
});

document.body.addEventListener('click', function() {
	// handle second thing
}

document.body.addEventListener('click', function() {
	// handle third thing	
}

So why not do that for ES6 proxies as well by adding an invoke trap.

If someone could give me a really good reason for why this is a bad idea, please do. I'm open minded.

# Kevin Smith (5 years ago)

So why not do that for ES6 proxies as well by adding an invoke trap.

Tom's response a couple of messages back summed it up fairly well.

To paraphrase, introducing "invoke" is a reasonable thing to propose (evidenced by the fact that it was part of the spec draft at one point). However, "invoke" in JS not a fundamental operation. It's always been a "get" followed by a "call". And since Proxies are all about allowing the user to define arbitrary behavior for the fundamental object model operations, it makes more sense to not have an "invoke" trap.

Could a userland library make writing these kinds of proxies more ergonomic?

# Tom Van Cutsem (5 years ago)

Edwin,

I sympathize with your point-of-view, but we've debated all the pros and cons of invoke long ago, and unless new arguments are brought to the table, I don't think it will be added again soon.

To clarify why the "invoke = get + call" equivalence is so important, consider for instance the common coding pattern of a "conditional" method call, where one only wants to invoke a method if it exists:

var foo = obj.foo;
if (foo) {
  foo.call(obj)
}

With the current Proxy API, the get trap returns a function (or a proxy for a function), and the code works fine regardless of whether foo() is called as a method, or as a function.

With the addition of invoke, if you don't also provide a get trap, then the code will not behave as expected. Likely obj.foo will return undefined, even though obj.foo() would have triggered the invoke trap and done something useful.

So, any robust use of invoke still requires implementing a get trap (to allow for all the use cases where methods are extracted as functions from objects). As a result, it would lead to a net increase in lines-of-code.

2015-06-11 15:48 GMT+02:00 Kevin Smith <zenparsing at gmail.com>:

Could a userland library make writing these kinds of proxies more ergonomic?

I don't see why not. The Proxy API offers only the bare minimum tools to create your own custom "exotic" objects. Arguably we need to grow some good libraries around it to make building these types of objects more routine (although I'd be the first to point out that you should avoid "exotic" objects unless there's a really compelling reason to go that route)

# Isiah Meadows (5 years ago)

To be honest, you could simply subclass Proxy to add an invoke hook, if it's that simple:

function makeHandler(handler) {
  let {get, invoke} = handler;
  let lastGet = false;
  let opts = {};
  Object.keys(opts)
    .filter(key => key !== 'get' && key !== 'call')
    .map(key => [key, handler[key]])
    .forEach(([key, func]) =>
        opts[key] = function () {
      lastGet = false;
      func.apply(this, arguments);
    });
  opts.get = function (target, property) {
    new Proxy(target[property], {
      apply(f, thisArg, args) {
        return invoke.call(handler, target, property, args);
      },
    });
    return get.apply(this, arguments);
  };
  return opts;
}

class InvokeProxy extends Proxy {
  constructor(target, handler) {
    super(target, makeHandler(handler));
  }

  static revocable(target, handler) {
    return Proxy.revocable(target, makeHandler(handler));
}

---------- Forwarded message ----------

From: Tom Van Cutsem <tomvc.be at gmail.com>

To: Kevin Smith <zenparsing at gmail.com>

Cc: es-discuss <es-discuss at mozilla.org>

Date: Thu, 11 Jun 2015 21:01:48 +0200 Subject: Re: ES6 Proxy Function Call Trap

Edwin,

I sympathize with your point-of-view, but we've debated all the pros and cons of invoke long ago, and unless new arguments are brought to the table, I don't think it will be added again soon.

To clarify why the "invoke = get + call" equivalence is so important, consider for instance the common coding pattern of a "conditional" method call, where one only wants to invoke a method if it exists:


var foo = obj.foo;

if (foo) {

  foo.call(obj)

}

With the current Proxy API, the get trap returns a function (or a proxy for a function), and the code works fine regardless of whether foo() is called as a method, or as a function.

With the addition of invoke, if you don't also provide a get trap, then the code will not behave as expected. Likely obj.foo will return undefined, even though obj.foo() would have triggered the invoke trap and done something useful.

So, any robust use of invoke still requires implementing a get trap (to allow for all the use cases where methods are extracted as functions from objects). As a result, it would lead to a net increase in lines-of-code.

2015-06-11 15:48 GMT+02:00 Kevin Smith <zenparsing at gmail.com>:

Could a userland library make writing these kinds of proxies more ergonomic?

I don't see why not. The Proxy API offers only the bare minimum tools to create your own custom "exotic" objects. Arguably we need to grow some good libraries around it to make building these types of objects more routine (although I'd be the first to point out that you should avoid "exotic" objects unless there's a really compelling reason to go that route)