As a first post, I want to explain a feature in my style of programming that leaves a few people puzzled when reading my code.
which defines a constructor for the “class” Class and can be invoked with
This is of course a rather shallow syntactic sugar for
A downside of this approach is that this loses its context when a method is passed in a different environment:
The reason is that this is not lexically but dynamically scoped, and bound by the caller upon invoking a function. This means, in the above example, this gets bound to the context where the function call happens. Running the snippet in a browser (preferably at the top level) and redefining the method as console.log(this) should reveal this. Additionally, when the function body is in strict mode, no default binding takes place. There are also many important but subtle details with implicit binding that I am not going to cover here.
To avoid this, we need a way to bind the context of every method to a lexical environment. To do so, we need closures.
A closure is a function bound to an environment. Environments are the set of free variables accessible by a function body. Simplifying, there here are two kinds of closures:
- dynamic closures, where free variables are bound to the context of the caller
- lexical closures, where free variables are bound to the lexical environment where the function is defined
Most often, the term “closure” implicitly refers to lexical closures, so I will use this convention for the rest of the article.
this shoud print 5. We can return closures that keep valid references to the free variables they refer to, regardless of where the function is called.
We can also write factory-style closure constructors:
Closures can be created anytime with completely different environments. We can return them as values, assign them to variables, and do anything we do with other JS values.
Objects in class-based OOP are commonly defined as being instances of a class, that incapsulate functionality and abstract implementation. This is achieved in the following way:
- classes have an internal structure which is not accessible directly by outside code;
- classes define an interface through which outside code must interact (methods);
- instances are newly created from the class definition, and only passed some values for initialization.
Code interacting with an instance of a class does not need to know how the functionality of a class is implemented. Only methods are allowed to interact with its internal structure; thus, methods have access to member variables as if they were defined as closures in the context of the class.
Objects as closures
We can use objects to rewrite the last code snippet:
So far so good. There is only one issue though: as I said early in the article, we cannot pass the print method to other functions without losing this. We can wrap the method using bind:
which returns a closure wrapping our method in the context of the object. While this solves our problem, we would like to pass methods without additional wrapping. Furthermore, when we pass a method as callback to a function outside our control (e.g. library or builtin function), there is no guarantee that the context will be preserved at every call site.
Closures are objects
To solve this, we must understand that objects are nothing more than syntactical sugar over plain closures. We don’t need this to write fully expressive code.
Using closures, we can implement every feature of class-based OOP without the pitfalls of dynamic scoping imposed by this semantics.
First, to encapsulate state we can wrap it in a closure
then, we want to implement class-like methods, our interface to the local state:
note that we don’t use this, as state is in scope we can reference to it by name.
Finally, we return an interface that outside code will use to interact with local state:
What we use to represent the interface does not matter: we could also use arrays, a function with switch, or just return the method if there’s only one.
Now we can construct our object like this:
Since this is a dynamically scoped variable, it is trivial to emulate the new operator in terms of closures:
We see that new is nothing more than a decorator over constructors, and that constructors are nothing more than plain functions.
The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."
Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress. On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.
- OOP in Scheme
- Introduction to Functional Programming Through Lambda Calculus
- Structure and Interpretation of Computer Programs