Changing The THIS Binding And Execution Context Of A Generator In Node.js
Now that I'm on one of the Node.js teams at InVision App, I get to start using some of the newer, more advanced features of JavaScript, like Generators and generator functions. In the past, I've looked at how Generators can be used to make asynchronous programming more visually pleasing. But, since I can also use the newer ES6 Class syntax, it got me thinking about the execution context of Generators. Specifically, how we can change the ".this" binding of a Generator object.
My first thought was to use JavaScript's .call() Function method to change the context of the .next() method during the consumption of the Generator:
// Require the core node modules.
var chalk = require( "chalk" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I create a Generator Object that tests the "this" binding.
function* genfunk() {
console.log( chalk.red.bold( "[Generator]" ) );
console.log( chalk.red( "[this]:" ), this );
}
var generator = genfunk();
// Now that the Generator Object has been created, let's see if we can change the
// context binding of the iterable method at execution time using .call().
// --
// CAUTION: THIS DOES NOT WORK (KEEP READING THE ARTICLE, BRO)
generator.next.call({
description: "I was bound to the generator using .next.call()."
})
// Let's test to see what ".this" binding is in place.
gen.next();
As it turns out, however, this approach doesn't work. It looks like Generators are a bit more "magical" than normal Objects (I mean, obviously, they can arbitrarily yield execution). So, when we try to run the above Node.js code, we get the following terminal error:
TypeError: Method [Generator].prototype.next called on incompatible receiver #<Object>
Once the Generator object is created, we can't mess with the execution context. But, as it turns out, we can use the .call() and .apply() methods to change the execution context at Generator creation time. Meaning, when we call the generator function, that returns the Generator object, we can change the execution context:
// Require the core node modules.
var chalk = require( "chalk" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I create a Generator Object that tests the "this" binding.
function* genfunk() {
console.log( chalk.red.bold( "[Generator]" ) );
console.log( chalk.red( "[this]:" ), this );
}
// When we invoke the generator function, we can use the .call() / .apply() methods to
// change the execution context of the resultant Generator Object. Once we do this, all
// calls to .next(), .throw(), and .return() will execute with the given .this binding.
var generator = genfunk.call({
description: "I was bound to the generator using .call()."
});
// Let's test to see what ".this" binding is in place.
generator.next();
As you can see, rather than calling the generator function directly, we're indirectly calling it through the use of the .call() Function method. This sets up our provided object as part of the execution context of the Generator; and, when we run the above code, we get the following terminal output:
As you can see, by using .call() to change the execution context of the generator function, it creates a Generator Object that is bound to the given context. As such, when we log "this" to the console, we get the object that was passed into the .call() method.
Now that we see that we can use .call() - or .apply() - to change the execution context of the resultant Generator Object, it got me thinking about the prototype chain. The Generator object might have a touch of magic to it; but, the prototype chain does not. As such, I wanted to see if I could use the prototype chain to dynamically change the execution context even after the Generator object was created.
To do this, I am going to provide an "intermediary context" as the "this" binding when invoking the generator function. This locks the intermediary context in as part of the execution context of the Generator object. But, we can use the ES6 Object.setPrototypeOf() method to dynamically change the prototype of the "intermediary context", thereby changing the prototypal inheritance of the Generator at execution time. This means that we can alter the prototype chain of the Generator in between our calls to .next():
CAUTION: This is just a fun experiment meant to learn me some things about JavaScript and ES6. I am not recommending that you do this. In fact, this will not behave as you expect since any WRITES to the prototype chain happen at the lowest level, which is probably not what you want.
// Require the core node modules.
var chalk = require( "chalk" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// I am a generator functions that yields 3 times, requiring 4 calls to .next() in order
// to be fully consumed.
function* genfunk() {
console.log( chalk.red.bold( "(1)" ), "this.description:", this.description );
yield;
console.log( chalk.red.bold( "(2)" ), "this.description:", this.description );
yield;
console.log( chalk.red.bold( "(3)" ), "this.description:", this.description );
yield;
console.log( chalk.red.bold( "(4)" ), "this.description:", this.description );
}
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// 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 PROTOTYPAL INHERITANCE
// to expose a portion of the prototype chain that we can change. This "proxy context"
// will be the execution context of the Generator Object throughout the entire execution;
// however, we'll be able to CHANGE THE PROTOTYPE OF THE PROXY in order to dynamically
// change the context of the Generator.
// --
// CAUTION: Due to the way prototypal inheritance works, if you SET A VALUE INTO THE
// PROTOTYPE CHAIN, it will be stored in the closest link in the prototype chain. As
// such, you can read from the higher-up prototypes, but, you can't write to them.
var proxyContext = Object.create( null );
// 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 );
// Now, let's inject a convenience method into the Generator Object that will allows us
// to change the prototype chain at execution time.
generator.bindTo = function( newContext ) {
Object.setPrototypeOf( proxyContext, newContext );
};
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// These are the various prototypal contexts that we'll be using to alter the execution
// binding of the Generator as it starts to yield values.
var contextA = {
description: "I am context A."
};
var contextB = {
description: "I am context B."
};
var contextC = {
description: "I am context C."
};
// Now that our Generator Object has been built using a this-context that can be swapped
// at any time, let's try consuming the values, changing the context between each of the
// .next() invocations.
generator.next();
generator.bindTo( contextA );
generator.next();
generator.bindTo( contextB );
generator.next();
generator.bindTo( contextC );
generator.next();
As you can see, I am still using the .call() method to change the execution context of the Generator object when it is created from the generator function. But, this time, I'm also injecting a .bindTo() method which will dynamically change the prototype chain of the Generator object. This allows us to change the prototype chain traversal in between calls to .next(). And, when we run the above code, we get the following output:
AGAIN CAUTION: This will change the READs but not the WRITEs on the prototype chain.
As you can see, when we change the prototype chain in between calls to .next(), we change the value that is read out of "this.description".
Changing the prototype chain is just a fun experiment, but it's not something that I think solves an actual problem. Setting the initial execution context and "this" binding of the Generator Object using the .call() method on the generator function, however, is extremely helpful, especially when we start mixing the use of generators in with the use of "classes" in Node.js.
Want to use code from this post? Check out the license.
Reader Comments
@All,
I just wanted to do a quick follow-up on this post. In the current version, as I stated VERY CLEARLY, messing with the Prototype chain will work for Reads, but not for Writes. Well, if we use the ES6 Proxy object as part of the Generator execution context, we can successfully change both Reads and Writes:
www.bennadel.com/blog/3262-using-proxy-objects-to-dynamically-change-the-this-binding-within-a-generator-in-node-js.htm
To be clear, I can't think of a reason to do this; but, it was fun to play around with Proxy objects for the firs time. And, the more I can wrap my head around Generator objects, the better.
This is just awesome. I was groping my way down a similar path of ideas, so I guess you could say this post is like my own personal .bind() in my head.
@Shawn,
Very cool -- glad I could help. I am curious what kind of mad-science you are up to :D I haven't stuck with Generators all that much as I've mostly started using
async
/await
for the asynchronous processing.I've had some downtime recently and wanted to explore some language features of javascript that I haven't gotten to use - definitely not doing anything serious here. I had a lot of fun with generators last week and kept groping around for ideas to make them do just a little bit more. So I really enjoyed the idea of dynamically switching their context. I ended up mashing together a function the returned a class expression that abstracted over a proxy/generator train-wreck. When I fleshed it out, I was like "Nobody will ever, ever see this." Symbols were involved as well.
But it sounds like I am in much the same place: as fun as these things are, I also see that their practical use in production is probably pretty limited - at least on projects I tend to work on.
@Shawn,
Ha ha, this is like literally 80% of what I do with my spare time :P I find it fascinating to see how the language can work. And, most of it is crazy-pants; but, there's always a little nugget of gold mixed in with all the mud. Regardless, it's great that asynchronous workflows are getting easier and easier to write in JavaScript.
I still have yet to use a
Symbol
for anything. I am still not even entirely sure what they do.