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:

  1. trapTarget - the function being executed (the proxy’s target)
  2. thisArg - the value of this inside of the function during the call
  3. argumentsList - 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:

  1. trapTarget - the function being executed (the proxy’s target)
  2. 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:

  1. let target = function() { return 42 },
  2. proxy = new Proxy(target, {
  3. apply: function(trapTarget, thisArg, argumentList) {
  4. return Reflect.apply(trapTarget, thisArg, argumentList);
  5. },
  6. construct: function(trapTarget, argumentList) {
  7. return Reflect.construct(trapTarget, argumentList);
  8. }
  9. });
  10. // a proxy with a function as its target looks like a function
  11. console.log(typeof proxy); // "function"
  12. console.log(proxy()); // 42
  13. var instance = new proxy();
  14. console.log(instance instanceof proxy); // true
  15. 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:

  1. // adds together all arguments
  2. function sum(...values) {
  3. return values.reduce((previous, current) => previous + current, 0);
  4. }
  5. let sumProxy = new Proxy(sum, {
  6. apply: function(trapTarget, thisArg, argumentList) {
  7. argumentList.forEach((arg) => {
  8. if (typeof arg !== "number") {
  9. throw new TypeError("All arguments must be numbers.");
  10. }
  11. });
  12. return Reflect.apply(trapTarget, thisArg, argumentList);
  13. },
  14. construct: function(trapTarget, argumentList) {
  15. throw new TypeError("This function can't be called with new.");
  16. }
  17. });
  18. console.log(sumProxy(1, 2, 3, 4)); // 10
  19. // throws error
  20. console.log(sumProxy(1, "2", 3, 4));
  21. // also throws error
  22. 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:

  1. function Numbers(...values) {
  2. this.values = values;
  3. }
  4. let NumbersProxy = new Proxy(Numbers, {
  5. apply: function(trapTarget, thisArg, argumentList) {
  6. throw new TypeError("This function must be called with new.");
  7. },
  8. construct: function(trapTarget, argumentList) {
  9. argumentList.forEach((arg) => {
  10. if (typeof arg !== "number") {
  11. throw new TypeError("All arguments must be numbers.");
  12. }
  13. });
  14. return Reflect.construct(trapTarget, argumentList);
  15. }
  16. });
  17. let instance = new NumbersProxy(1, 2, 3, 4);
  18. console.log(instance.values); // [1,2,3,4]
  19. // throws error
  20. 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:

  1. function Numbers(...values) {
  2. if (typeof new.target === "undefined") {
  3. throw new TypeError("This function must be called with new.");
  4. }
  5. this.values = values;
  6. }
  7. let instance = new Numbers(1, 2, 3, 4);
  8. console.log(instance.values); // [1,2,3,4]
  9. // throws error
  10. 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:

  1. function Numbers(...values) {
  2. if (typeof new.target === "undefined") {
  3. throw new TypeError("This function must be called with new.");
  4. }
  5. this.values = values;
  6. }
  7. let NumbersProxy = new Proxy(Numbers, {
  8. apply: function(trapTarget, thisArg, argumentsList) {
  9. return Reflect.construct(trapTarget, argumentsList);
  10. }
  11. });
  12. let instance = NumbersProxy(1, 2, 3, 4);
  13. 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:

  1. class AbstractNumbers {
  2. constructor(...values) {
  3. if (new.target === AbstractNumbers) {
  4. throw new TypeError("This function must be inherited from.");
  5. }
  6. this.values = values;
  7. }
  8. }
  9. class Numbers extends AbstractNumbers {}
  10. let instance = new Numbers(1, 2, 3, 4);
  11. console.log(instance.values); // [1,2,3,4]
  12. // throws error
  13. 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:

  1. class AbstractNumbers {
  2. constructor(...values) {
  3. if (new.target === AbstractNumbers) {
  4. throw new TypeError("This function must be inherited from.");
  5. }
  6. this.values = values;
  7. }
  8. }
  9. let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
  10. construct: function(trapTarget, argumentList) {
  11. return Reflect.construct(trapTarget, argumentList, function() {});
  12. }
  13. });
  14. let instance = new AbstractNumbersProxy(1, 2, 3, 4);
  15. 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:

  1. class Person {
  2. constructor(name) {
  3. this.name = name;
  4. }
  5. }
  6. let PersonProxy = new Proxy(Person, {
  7. apply: function(trapTarget, thisArg, argumentList) {
  8. return new trapTarget(...argumentList);
  9. }
  10. });
  11. let me = PersonProxy("Nicholas");
  12. console.log(me.name); // "Nicholas"
  13. console.log(me instanceof Person); // true
  14. 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.