Method Binding Is An Implicit Part Of Your API Contract (Whether You Like It Or Not)

The other day, when I was investigating the Zendesk web widget race condition, I had originally come up with a solution that passed the zEmbed object methods into a setTimeout() call as so-called "naked method" references. This approach worked; but, it got me thinking about API contracts. In the Zendesk web widget documentation, there is no mention about whether or not its methods can be passed-around as naked references. So, it begs the question: is it actually OK to use them that way, regardless of whether or not it works. The more I consider this question, the more I feel that your object construction - and its inherent method binding approach - is an implicit part of your API contract, whether you document it or not. Method binding is so fundamental to the understanding of JavaScript, I think you have to expect people to try to use it in a variety of ways.

To be clear, when I use the term "naked method", I am talking about a direct reference to a Function object. Specifically, one that will be invoked as an unscoped "function" and not as part of an "object method" relationship. With the zEmbed demo from the other day, the "naked method" solution that I toyed with passed the show() and hide() API methods as naked references to a setTimeout() call. Something like:

setTimeout( zEmbed**.show**, 500 );

Notice that I am not invoking show() - I'm passing around "show" as a direct Function reference. This way, when the setTimeout() timer goes to invoke the callback - show() - it will do so outside of the zEmbed object context.

With the zEmbed object, this works because, presumably, the show() and hide() methods are using closures and lexical-binding in order to wire-up their internal references. This is not an unusual approach. Many Promise / Deferred libraries do this specifically so that their resolve and reject methods can be passed around as naked function references (see jQuery Deferred, see AngularJS $q).

NOTE: I am not actually sure if the above libraries are using lexical binding or if they are calling .bind() on the functions before exposing them; either way, the outcome is the same.

But, this object architecture is, more often than not, unstated. Meaning, the documentation for an object API almost never discusses the actual mechanics of the object internals. Now, you could argue that anything undocumented is dangerous to depend on. But, I would argue that object construction is such a fundamental part of the JavaScript language that your choice of object construction becomes an implicit part of your API whether or not you document it. And furthermore, that any change to your internal object construction is inherently a "breaking change" for your API.

To see how object architecture - and changes to it - affect consuming code, let's walk through some simple examples in Node.js. First, let's create a mock zEmbed object that uses lexical binding for all of its internal references:

// Require the core node modules. var chalk = require( "chalk" ); // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // I am a mock zEmbed object constructor. function ZEmbed( token ) { // Return the public API for this object. // -- // NOTE: The public API is a collection of functions that are using lexical binding // in order to locate "class variables". return({ hide: hide, show: show }); // --- // PUBLIC METHODS. // --- function hide() { console.log( chalk.red( `Hiding widget ${ token }.` ) ); } function show() { console.log( chalk.green( `Showing widget ${ token }.` ) ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // var zEmbed = new ZEmbed( "9cf6672aaf" ); // Let's detach the methods from the object for extra clarity on binding. var show = zEmbed.show; var hide = zEmbed.hide; // Because the zEmbed methods are using lexical binding, the methods can be passed // around as "naked" references. This feature is an IMPLICIT part of your object API. setTimeout( show, 500 ); setTimeout( hide, 1000 );

Here, you can see that the show() and hide() methods make no use of the "this" keyword. Instead, they are using lexical binding to locate the "token" instance property. This way, when the show() and hide() methods are invoked as naked functions, everything works as expected:

When it comes to object architecture, it's not an all-or-nothing approach. Meaning, we can create an object that uses both lexical and context-based bindings. For example, we can add method-chaining to the previous code by returning (this) from the individual methods:

// Require the core node modules. var chalk = require( "chalk" ); // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // I am a mock zEmbed object constructor. function ZEmbed( token ) { // Return the public API for this object. // -- // NOTE: The public API is a collection of functions that are using lexical binding // in order to locate "class variables". However, the public method do return a // reference back to the public API (this) for method chaining. return({ hide: hide, show: show }); // --- // PUBLIC METHODS. // --- function hide() { console.log( chalk.red( `Hiding widget ${ token }.` ) ); // Return this object to facilitate method chaining. return( this ); } function show() { console.log( chalk.green( `Showing widget ${ token }.` ) ); // Return this object to facilitate method chaining. return( this ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // var zEmbed = new ZEmbed( "9cf6672aaf" ); // Let's detach the methods from the object for extra clarity on binding. var show = zEmbed.show; var hide = zEmbed.hide; // Because the zEmbed methods are using lexical binding, the methods can be passed // around as "naked" references. This feature is an IMPLICIT part of your object API. setTimeout( show, 500 ); setTimeout( hide, 1000 ); // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // Even though the zEmbed class methods are using lexical binding to locate the class // properties, they are also using the this-binding to return a reference back to the // public API. This allows for method chaining, but ONLY IF you invoke the methods in // the context of the public API. setTimeout( () => { // Try method-chaining in the context of the API. console.log( chalk.bold( "

== Trying method chaining ==" ) ); zEmbed.show().hide(); // Try method-chaining with the naked method references. console.log( chalk.bold( "

== Trying NAKED method chaining ==" ) ); show().hide(); }, 2000 );

In this case, when the show() and hide() methods return (this), they are returning a reference to the context binding which is the object on which the methods were invoked. This gives the show() and hide() functions a bit of a dual-nature. On one hand, they are still lexically bound to the "token" reference and to each other; but, on the other hand, they are contextually bound based on their invocation. This has a direct impact on how the naked references can be used. And, when we run the above code, we get the following output:

As you can see, the naked function references can still be invoked on their own without breaking. However, in order to use the method chaining feature, we have to invoke the methods in the context of the zEmbed object (or, more specifically, its public API). Otherwise, the "this" reference is not bound to the correct object and the chained method cannot be found.

Now, if we continue to evolve this code and decide to replace the lexically-bound methods with full-on "class methods", the concept of the naked function reference completely breaks:

// Require the core node modules. var chalk = require( "chalk" ); // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // // I am a mock zEmbed class. // -- // NOTE: With a traditional "class", all of the internal references use "this" because // the methods are all located on the class prototype and do not have a lexical binding // to any of the other methods or instance variables. class ZEmbed { constructor( token ) { this.token = token; } // --- // PUBLIC METHODS. // --- hide() { console.log( chalk.red( `Hiding widget ${ this.token }.` ) ); // Return this object to facilitate method chaining. return( this ); } show() { console.log( chalk.green( `Showing widget ${ this.token }.` ) ); // Return this object to facilitate method chaining. return( this ); } } // ----------------------------------------------------------------------------------- // // ----------------------------------------------------------------------------------- // var zEmbed = new ZEmbed( "9cf6672aaf" ); // Let's detach the methods from the object for extra clarity on binding. var show = zEmbed.show; var hide = zEmbed.hide; // Because the zEmbed methods are part of a traditional class bindings, the following // calls WILL BREAK. The methods are being executed outside of their expected context. setTimeout( show, 500 ); setTimeout( hide, 1000 );

As you can see, this time, we're using a more traditional / classical style of object creation in which we're setting up the Prototype for instance methods and using the "this" keyword with all internal references. Now, when we go to use the class methods as naked functions, we get the following terminal output:

Here, you can see that we can no longer use of the show() and hide() methods as naked function references. This is because the "this" reference is not bound to the class instance if the method is invoked outside the context of the class.

The point of all this is just to demonstrate that - as a fundamental feature of JavaScript - your internal object structure has a direct impact on how your API can be consumed. And, as you evolve your internal object structure, you run the risk of breaking consuming code. As such, your internal object structure and your method binding approach is an implicit part of your API whether or not you intended it to be. And, as you evolve your internal structure, you have to be cognizant of whether or not it amounts to a breaking change.

Tweet This Great article by @BenNadel - Method Binding Is An Implicit Part Of Your API Contract (Whether You Like It Or Not) Woot woot — you rock the party that rocks the body!







