Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Dan Sirucek
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Dan Sirucek

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

By
Published in Comments (2)

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:

Your method binding approach can affect the way your API is consumed.

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( "\n== Trying method chaining ==" ) );
		zEmbed.show().hide();

		// Try method-chaining with the naked method references.
		console.log( chalk.bold( "\n== 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:

Your method binding approach can affect the way your API is consumed.

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:

Your method binding approach can affect the way your API is consumed.

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.

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

Reader Comments

17 Comments

I think the design of the API can subtly influence the intended use.

For example, if I'm calling a method on a global object, or one that was simply available without directly being involved in creating it, it feels "safer" to use naked method calls. For example, if you can call `window.Foo.open()`.

I think this is partially because, in JS, that's a common way to namespace functionality, so the initial object is really just a namespace, not a true instance of another object.

On the other hand, if the API requires you to call "new Foo" or a method called "createFoo()" to get access to something, it's sort of implied the returning object may not support naked method calls. Now you clearly have a new object, which is encapsulating some data that it can work on.

15,848 Comments

@Phil,

That's a really interesting point. I hadn't really considered the way in which the object was "created" as an indication of how it can be used; but, I think you're absolutely right. If I have to "new" an object, it definitely feels much less like I can pass around methods because it feels like they were intended to be associated with an instance. However, if I have some global object that just _exists_, then it does feel much safer to pass methods around.

Excellent point!

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