Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Edith Au
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Edith Au

Using Proxy Objects To Dynamically Change The THIS Binding Within A Generator In Node.js

By
Published in

CAUTION: This is just a fun learning experiment. Do not take this seriously.

Yesterday, I looked at how we can change the execution context of a Generator at creation time by invoking the generator function using .call() or .apply(). As part of that experiment, I also used the prototype chain of the Generator to change the way the READS work, explicitly calling out the fact that WRITES will break. Well, as an even moar fun follow-up, I wanted to play with the ES6 Proxy object as a way to dynamically change the execution context of a Generator, in Node.js, while keeping both READ and WRITE functionality in tact. Basically, this is just an excuse for me to play around with the Proxy object.

A Proxy object is basically a wrapper around another Object - a Target object - that allows calls to the Target object to be intercepted. Once intercepted, these calls can be blocked, altered, or passed onto another Object which may or may not be the original Target object. Since Proxies are so dynamic, I wanted to try and use a Proxy to create the execution context of a Generator; then, intercept all calls to that generator and pass those calls onto a dynamically allocated "this" context:

Using a Proxy object as part of the execution context of a Generator Object in ES6 and Node.js.

In order to do this, we need to be able to change the Target of the Proxy after the proxy has been created. Out of the box, the ES6 Proxy implementation doesn't support this functionality. But, using the "traps" in the Proxy handler, we can certainly intercept calls that can be used to change a lexically-bound Target variable. To do this, I created a simple SwappableProxy() class. The constructor of this class sets up (and returns) the Proxy object; and, the static methods on this class provide a way to dynamically change the Target:

// I create a Proxy object whose "target" can be dynamically changed at runtime using
// the SwappableProxy.setTargetOf() static method.
// --
// NOTE: For this exploration, I am not defining all possible Proxy handlers - just
// the basic getter / setter ones.
class SwappableProxy {

	// I initialize the swappable proxy.
	constructor( target = null ) {

		var proxy = new Proxy(
			{}, // This value is irrelevant, it just has to be valid.
			{
				deleteProperty: function( _, property ) {

					delete( target[ property ] );

					// Return True to indicate that the delete was successful.
					return( true );

				},
				get: function( _, property, receiver ) {

					if ( property === "__proxy_target__" ) {

						return( target );

					}

					return( target[ property ] );

				},
				has: function( _, property ) {

					return( property in target );

				},
				set: function( _, property, value, receiver ) {

					if ( property === "__proxy_target__" ) {

						target = value;

					}

					target[ property ] = value;

					// Return True to indicate that the set was successful.
					return( true );

				}
			}
		);

		return( proxy );

	}

	// ---
	// STATIC METHODS.
	// ---

	// I get the current target of the swappable proxy.
	static getTargetOf( proxy ) {

		return( proxy.__proxy_target__ );

	}

	// I set a new target for the swappable proxy.
	static setTargetOf( proxy, target ) {

		proxy.__proxy_target__ = target;

	}

}

For this exploration, I only defined the "trap" functions that center around basic property access and mutation - there are a handful of other traps that can be defined in a Proxy. In this case, each of the traps intercepts the call and forwards it onto the lexically-bound Target variable. This Target variable can then be changed over time, thereby altering the final destination of these intercepted calls.

Once I had this SwappableProxy() class, I could then used it to define the execution context of a Generator Object. By keeping a reference to the proxy, it allows me to alter the behavior of the "this." reference in the generator function arbitrarily. To test this, I created a generator function with several "yield" statements. I then use the SwappableProxy() class to change the Proxy target in between each "yield" call:

// Require the core node modules.
var chalk = require( "chalk" );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// I am generator function that READS and WRITES to the THIS reference. It will yield
// three times before completion.
function* genfunk() {

	// READ: this.description / WRITE: this.counter.
	console.log( chalk.red.bold( "(1)" ), "this.description:", this.description );
	console.log( chalk.dim.italic( "(1) Incrementing this.counter." ) );
	this.counter++;
	yield;

	// READ: this.description / WRITE: this.counter.
	console.log( chalk.red.bold( "(2)" ), "this.description:", this.description );
	console.log( chalk.dim.italic( "(2) Incrementing this.counter." ) );
	this.counter++;
	yield;

	// READ: this.description / WRITE: this.counter.
	console.log( chalk.red.bold( "(3)" ), "this.description:", this.description );
	console.log( chalk.dim.italic( "(3) Incrementing this.counter." ) );
	this.counter++;

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// I create a Proxy object whose "target" can be dynamically changed at runtime using
// the SwappableProxy.setTargetOf() static method.
// --
// NOTE: For this exploration, I am not defining all possible Proxy handlers - just
// the basic getter / setter ones.
class SwappableProxy {

	// I initialize the swappable proxy.
	constructor( target = null ) {

		var proxy = new Proxy(
			{}, // This value is irrelevant, it just has to be valid.
			{
				deleteProperty: function( _, property ) {

					delete( target[ property ] );

					// Return True to indicate that the delete was successful.
					return( true );

				},
				get: function( _, property, receiver ) {

					if ( property === "__proxy_target__" ) {

						return( target );

					}

					return( target[ property ] );

				},
				has: function( _, property ) {

					return( property in target );

				},
				set: function( _, property, value, receiver ) {

					if ( property === "__proxy_target__" ) {

						target = value;

					}

					target[ property ] = value;

					// Return True to indicate that the set was successful.
					return( true );

				}
			}
		);

		return( proxy );

	}

	// ---
	// STATIC METHODS.
	// ---

	// I get the current target of the swappable proxy.
	static getTargetOf( proxy ) {

		return( proxy.__proxy_target__ );

	}

	// I set a new target for the swappable proxy.
	static setTargetOf( proxy, target ) {

		proxy.__proxy_target__ = target;

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// Once the Generator Object is created by the generator function, we can't change the
// execution context of it directly. However, we can set the execution context of the
// Generator Object at creation time. This means that we can use a SWAPPABLE PROXY as
// part of the execution context. Then, we'll be able to change the TARGET OF THE PROXY
// during the program's execution. This will dynamically change the way the THIS
// reference behaves within the generator function.
var proxyContext = new SwappableProxy()

// Create the Generator Object using the "proxy" as the execution context (ie, the
// "proxyContext" will be used as the "this" binding internally).
var generator = genfunk.call( proxyContext );

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// These are the various "pass through" contexts that our Proxy object will interact
// with as the THIS reference is consumed in between the yield statements within the
// Generator object. The DESCRIPTION property will be read from and the COUNTER
// property will be written to.
var contextA = {
	description: "I am context A.",
	counter: 0
};
var contextB = {
	description: "I am context B.",
	counter: 0
};
var contextC = {
	description: "I am context C.",
	counter: 0
};

// Now that our Generator Object has been built using a this-context that we can
// dynamically proxy a runtime, let's try consuming the values while changing the
// proxy target between each of the .next() invocations.
SwappableProxy.setTargetOf( proxyContext, contextA );
generator.next();

SwappableProxy.setTargetOf( proxyContext, contextB );
generator.next();

SwappableProxy.setTargetOf( proxyContext, contextC );
generator.next();

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// After our Generator has been fully consumed, let's output the various context
// objects to see how they were mutated. Remember, we called the "++" operator three
// times, but swapped the Proxy target in between each call.
console.log( chalk.red.bold( "[ContextA]:" ), "this.counter:", contextA.counter );
console.log( chalk.red.bold( "[ContextB]:" ), "this.counter:", contextB.counter );
console.log( chalk.red.bold( "[ContextC]:" ), "this.counter:", contextC.counter );

As you can see, in between each yield call I'm performing three instance access operations:

  • READ: this.description
  • READ: this.counter
  • WRITE: this.counter

NOTE: The "++" operator performs a READ and a WRITE of the operand property.

But, in between each call to the Generator object's .next() method, I'm swapping out the underlying context, thereby changing the behavior of "this" references. And, when we run the above code, we get the following terminal output:

Using a Proxy object as part of the execution context of a Generator Object in ES6 and Node.js.

As you can see, it worked as we expected: reads for this.description and this.counter were read from the "current" context and the final this.counter value was written to the "current" context.

I can't think of a reason that I would actually want to change the execution context of a Generator Object mid-execution. But, regardless of use-case, this was a great way to start learning about Proxy objects in ES6; and, about the extremely dynamic power of the "trap" functions that Proxy objects can implement.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel