Function Proxies with the apply
and construct
Traps
Of all the proxy traps, only apply
and construct
require the proxy target to be a function. Recall from Chapter 3 that functions have two internal methods called [[Call]]
and [[Construct]]
that are executed when a function is called without and with the new
operator, respectively. The apply
and construct
traps correspond to and let you override those internal methods. When a function is called without new
, the apply
trap receives, and Reflect.apply()
expects, the following arguments:
trapTarget
- the function being executed (the proxy’s target)thisArg
- the value ofthis
inside of the function during the callargumentsList
- an array of arguments passed to the function
The construct
trap, which is called when the function is executed using new
, receives the following arguments:
trapTarget
- the function being executed (the proxy’s target)argumentsList
- an array of arguments passed to the function
The Reflect.construct()
method also accepts these two arguments and has an optional third argument called newTarget
. When given, the newTarget
argument specifies the value of new.target
inside of the function.
Together, the apply
and construct
traps completely control the behavior of any proxy target function. To mimic the default behavior of a function, you can do this:
let target = function() { return 42 },
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// a proxy with a function as its target looks like a function
console.log(typeof proxy); // "function"
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
This example has a function that returns the number 42. The proxy for that function uses the apply
and construct
traps to delegate those behaviors to the Reflect.apply()
and Reflect.construct()
methods, respectively. The end result is that the proxy function works exactly like the target function, including identifying itself as a function when typeof
is used. The proxy is called without new
to return 42 and then is called with new
to create an object called instance
. The instance
object is considered an instance of both proxy
and target
because instanceof
uses the prototype chain to determine this information. Prototype chain lookup is not affected by this proxy, which is why proxy
and target
appear to have the same prototype to the JavaScript engine.
Validating Function Parameters
The apply
and construct
traps open up a lot of possibilities for altering the way a function is executed. For instance, suppose you want to validate that all arguments are of a specific type. You can check the arguments in the apply
trap:
// adds together all arguments
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// throws error
console.log(sumProxy(1, "2", 3, 4));
// also throws error
let result = new sumProxy();
This example uses the apply
trap to ensure that all arguments are numbers. The sum()
function adds up all of the arguments that are passed. If a non-number value is passed, the function will still attempt the operation, which can cause unexpected results. By wrapping sum()
inside the sumProxy()
proxy, this code intercepts function calls and ensures that each argument is a number before allowing the call to proceed. To be safe, the code also uses the construct
trap to ensure that the function can’t be called with new
.
You can also do the opposite, ensuring that a function must be called with new
and validating its arguments to be numbers:
function Numbers(...values) {
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentList) {
throw new TypeError("This function must be called with new.");
},
construct: function(trapTarget, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.construct(trapTarget, argumentList);
}
});
let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
NumbersProxy(1, 2, 3, 4);
Here, the apply
trap throws an error while the construct
trap uses the Reflect.construct()
method to validate input and return a new instance. Of course, you can accomplish the same thing without proxies using new.target
instead.
Calling Constructors Without new
Chapter 3 introduced the new.target
metaproperty. To review, new.target
is a reference to the function on which new
is called, meaning that you can tell if a function was called using new
or not by checking the value of new.target
like this:
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
Numbers(1, 2, 3, 4);
This example throws an error when Numbers
is called without using new
, which is similar to the example in the “Validating Function Parameters” section but doesn’t use a proxy. Writing code like this is much simpler than using a proxy and is preferable if your only goal is to prevent calling the function without new
. But sometimes you aren’t in control of the function whose behavior needs to be modified. In that case, using a proxy makes sense.
Suppose the Numbers
function is defined in code you can’t modify. You know that the code relies on new.target
and want to avoid that check while still calling the function. The behavior when using new
is already set, so you can just use the apply
trap:
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
The NumbersProxy
function allows you to call Numbers
without using new
and have it behave as if new
were used. To do so, the apply
trap calls Reflect.construct()
with the arguments passed into apply
. The new.target
inside of Numbers
is equal to Numbers
itself, and no error is thrown. While this is a simple example of modifying new.target
, you can also do so more directly.
Overriding Abstract Base Class Constructors
You can go one step further and specify the third argument to Reflect.construct()
as the specific value to assign to new.target
. This is useful when a function is checking new.target
against a known value, such as when creating an abstract base class constructor (discussed in Chapter 9). In an abstract base class constructor, new.target
is expected to be something other than the class constructor itself, as in this example:
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers {}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// throws error
new AbstractNumbers(1, 2, 3, 4);
When new AbstractNumbers()
is called, new.target
is equal to AbstractNumbers
and an error is thrown. Calling new Numbers()
still works because new.target
is equal to Numbers
. You can bypass this restriction by manually assigning new.target
with a proxy:
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList, function() {});
}
});
let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
The AbstractNumbersProxy
uses the construct
trap to intercept the call to the new AbstractNumbersProxy()
method. Then, the Reflect.construct()
method is called with arguments from the trap and adds an empty function as the third argument. That empty function is used as the value of new.target
inside of the constructor. Because new.target
is not equal to AbstractNumbers
, no error is thrown and the constructor executes completely.
Callable Class Constructors
Chapter 9 explained that class constructors must always be called with new
. That happens because the internal [[Call]]
method for class constructors is specified to throw an error. But proxies can intercept calls to the [[Call]]
method, meaning you can effectively create callable class constructors by using a proxy. For instance, if you want a class constructor to work without using new
, you can use the apply
trap to create a new instance. Here’s some sample code:
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
The PersonProxy
object is a proxy of the Person
class constructor. Class constructors are just functions, so they behave like functions when used in proxies. The apply
trap overrides the default behavior and instead returns a new instance of trapTarget
that’s equal to Person
. (I used trapTarget
in this example to show that you don’t need to manually specify the class.) The argumentList
is passed to trapTarget
using the spread operator to pass each argument separately. Calling PersonProxy()
without using new
returns an instance of Person
; if you attempt to call Person()
without new
, the constructor will still throw an error. Creating callable class constructors is something that is only possible using proxies.