Object-Oriented Closures
In the past, I examined the problems with JavaScript’s this
keyword, and showed that none of the available solutions to its problems were comprehensively effective. (Click here to read that analysis.)
Today, using the ancestry of JavaScript for reference and justification, I present a solution for reliably writing object-oriented code in the language.
Using this
, we get caught up in a struggle to reference “the current object.” But this is not even an issue that needs to be addressed. Since JavaScript has first-class functions, we don’t need references to objects. Functions’ lexical closures are sufficient for writing object-oriented code in JavaScript.
Why do we even have this
?
In my last article, I compared programmers’ obsession with this
to a long-outgrown childish affection for a stuffed animal. We have this
because of emotional attachments that people developed to prior languages, and we still use this
because it’s become “normal.” Language users forced compromises between themselves and the language designer intelligentsia, who would have otherwise made the best decisions for us. We got the syntax that people wanted, rather than the syntax we needed.
Historical Aside
Brendan Eich, when designing a language for Netscape, imagined he would create “Scheme for the browser.” Scheme was Guy Steele and Gerald Sussman’s Lisp dialect used to teach abstraction fundamentals at MIT. However, a series of past events would ultimately warp Eich’s initial vision.
Years ago, Intel’s products won in the market because they remained backwards-compatible with older systems. Their architecture and assembly language became the basis for most software. The C language mapped closely to assembly, providing excellent performance that made it popular in the days of emerging computing power. C++ rode on the coattails of C; it was also fast, and compatible with C, but it had “more stuff,” which is a selling point for technology in America. Among that stuff was an object-oriented programming model. That model was copied into Java. Finally, in a landscape where the cascade of C, C++, and Java defined programmers’ understanding of linguistics, the nested parentheses of Scheme had to be replaced with curly braces and semicolons. Alonzo Church’s grandchild, the lambda function, was gilded with new
and this
and class
, the propeller beanies made fashionable by the new generation.
Never minding all that nonsense, Eich hid Scheme’s (lambda (x))
in plain sight. Church’s seed survives as function (x) { return … }
. With it, we retain Steele and Sussman’s building block for modular systems.
And here we are, with this bizarre this
dynamic value sprouting like a weed in a lexically-scoped language. this
has never worked quite right, because it was never the right feature for us to use in the first place. A more elegant object-oriented programming model has been available in the language since Day 1. The model may have been more obvious if not for Java’s syntactic corruption, which turned JavaScript into “Scheme in C’s clothing.”
As in JavaScript’s true ancestor—Scheme—we may use functions alone to create abstractions.
Scheme’s Functional Object-Oriented Programming Model
First, a primer on Lisp and Scheme.
Lisp syntax uses parenthesized lists to represent functions and data. When evaluating Lisp code, the first thing in a list is a function to call, and latter things in a list are arguments to pass. Unlike in languages that use keywords for definitions and conditional logic, functions serve these roles in Lisp. Functions are even used for mathematical calculations. In Lisp, there are no “blocks;” instead, there are just additional items in lists. A function body, which is a list of lists, is evaluated by evaluating each of its sublists in order.
Here is a Fibonnaci calculator written in Scheme:
(define (fib n)
(if (<= n 2)
1
(+ (fib (- n 1)) (fib (- n 2)))))
(The above Scheme code is equivalent to the following JavaScript code:)
function fib (n) {
if (n <= 2) {
return 1
} else {
return fib(n - 1) + fib(n - 2)
}
}
Lisp data is structured as linked lists composed of simple “pair” data structures called cons
cells. Each cons
has a head and tail; the head is retrieved with a function called car
, and the tail is retrieved with a function called cdr
.
(cons 1 2) ; creates a cons cell “(1 . 2)”
(car (cons 1 2)) ; returns 1
(cdr (cons 1 2)) ; returns 2
Multiple cons
linked together form lists.
(cons 1 (cons 2 (cons 3 '()))) ; creates a cons cell chain
; “(1 . (2 . (3 . ())))”,
; or rather the list “(1 2 3)”
(list 1 2 3) ; creates the list “(1 2 3)”,
; equivalent to the code above
Lists can represent more complex data structures; here’s an “association list,” a parallel to the hash maps ubiquitous in JavaScript:
((key1 . value1)
(key2 . value2))
In Steele and Sussman’s “Scheme” dialect of Lisp, it was shown that the basic data structure cons
(the basis for all others) could be implemented with closures created by functions.
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
(define (cdr z)
(z (lambda (p q) q)))
(The above Scheme code is equivalent to the following JavaScript code:)
function cons (x, y) {
return (m) => m(x, y)
}
function car (z) {
return z((p, q) => p)
}
function cdr (z) {
return z((p, q) => q)
}
A “cell” is formed by the creation of a lexical environment in which the names and values of the parameters x
and y
are retained by a returned anonymous function. cons
returns such anonymous functions; technically, this is only an implementation detail, and it is better to think of cons
as a pair; however, knowing that cons
is actually a closure/function explains how car
and cdr
work. Those functions take the cons
“pair”/function as an argument, and they call the cons
function with a “getter” function argument, and the getter retrieves from the cons
lexical environment either its head or tail value.
The equivalency between Scheme and JavaScript shows that, as in Scheme, closures can also be used to model complex data in JavaScript.
From JS To Scheme, Then Back Again
Let’s revisit the Employee
constructor from my previous post. In Scheme, the Employee
implementation might be transliterated as:
(define (employee tasks)
(let* ((acc 0)
(get-acc (lambda () acc))
(set-acc (lambda (x) (set! acc x))))
(lambda (m) (m tasks get-acc set-acc))))
(define (work employee)
(employee (lambda (tasks get-acc set-acc)
(for-each (lambda (task)
(set-acc (+ (get-acc) 1))) tasks)
(display (get-acc))))) ; 3
(define e (employee (list "this" "that" "the other thing")))
(work e)
Which would be equivalent to the following JavaScript code:
function Employee (tasks) {
var acc = 0
var getAcc = () => acc
var setAcc = (x) => acc = x
return (m) => m(tasks, getAcc, setAcc)
}
function work (employee) {
employee((tasks, getAcc, setAcc) => {
tasks.forEach((task) => setAcc(getAcc() + 1))
console.log(getAcc()) // 3
})
}
var employee = Employee(['this', 'that', 'the other thing'])
work(employee)
However, in JavaScript we tend to write our API’s a little differently than in Scheme. We don’t need to call a function with an object as its argument; instead, our constructor function can return an object with methods that act on data. Because we do this, using and modifying data in a function’s closure is a much simpler matter than in Scheme.
function Employee (tasks) {
var accomplishments = 0
function work () {
tasks.forEach(finishWork)
console.log(accomplishments) // 3
}
function finishWork () {
accomplishments++
}
return {work}
}
var employee = Employee(['this', 'that', 'the other thing'])
employee.work()
At last, we arrive at our closure-based model of object-oriented programming, totally free of this
. We don’t need any way to reference “the current object,” because for all intents and purposes, the lexical environment where tasks
and accomplishments
are defined is “the current object.” There isn’t any syntax which explicitly creates or references the closure; it exists and is used implicitly as a result of calling a “constructor” function. There is no need for prototype
or class
or new
or this
. The resulting code is simpler and has no risk of an incorrect this
binding.
Notice also how the issue of “calling methods with a caller” from my last post has been resolved. finishWork
, which manipulates the employee’s data, may simply be passed as an argument to forEach
, as finishWork
implicitly retains a reference to the accomplishments
binding.
Fly Away
We should set aside the unnecessary features that Netscape added to JavaScript merely to please the 90’s developer crowd. Schemers already had object-oriented programming figured out decades earlier. They didn’t need any special language features to create objects; lambdas represented objects. We can evangelize this fusion of the functional and object-oriented paradigms. We can invoke the simple genius of bygone minds which survives in the present day’s language.