Discussing the decisions surrounding JavaScript prototypes, the article by Juan Diego Rodriguez scrutinizes their origin, examines missteps in the design, and explores how these factors have affected the way we write JavaScript today.
Even though JavaScript’s name was coined from the Java language, the two languages are worlds apart. JavaScript has more in common with Lisp and Scheme, sharing features such as first-class functions and lexical scoping.
JavaScript also borrows its prototypal inheritance from the Self language. This inheritance mechanism is perhaps what many — if not most — developers do not spend enough time to understand, mainly because it isn’t a requirement to start working with JavaScript. That characteristic can be seen as either a design flaw or a stroke of genius. That said, JavaScript’s prototypal nature was marketed and hidden behind a “Java for the web” mask. We’ll elaborate more on that as we go on.
JavaScript isn’t confident in its own prototypal nature, so it gives developers the tools to approach the language without ever having to touch a prototype. This was an attempt to be easily understood by every developer, especially those coming from class-based languages, such as Java, and would later become one of JavaScript’s biggest enemies for years to come: You don’t have to understand how JavaScript works to code in JavaScript.
What Is Classical Object-Oriented Programming?
Classical object-oriented programming (OOP) revolves around the concept of classes and instances and is widely used in languages like Java, C++, C#, and many others. A class is a blueprint or template for creating objects. It defines the structure and behavior of objects that belong to that class and encapsulates properties and methods. On the other hand, objects are instances of classes. When you create an object from a class, you’re essentially creating a specific instance that inherits the structure and behavior defined in the class while also giving each object an individual state.
OOP has many fundamental concepts, but we will focus on inheritance, a mechanism that allows one class to take on the properties and methods of another class. This facilitates code reuse and the creation of a hierarchy of classes.
What’s Prototypal OOP In JavaScript?
I will explain the concepts behind prototypal OOP in Javascript, but for an in-depth explanation of how prototypes work, MDN has an excellent overview on the topic.
Prototypal OOP differs from classical OOP, which is based on classes and instances. In prototypal OOP, there are no classes, only objects, and they are created directly from other objects.
If we create an object, it will have a built-in property called prototype
that holds a reference to its “parent” object prototype so we can
access its prototype’s methods and properties. This is what allows us to
access methods like .sort()
or .forEach()
from any array since each array inherits methods from the Array.prototype
object.
The prototype itself is an object, so the prototype will have its own prototype. This creates a chain of objects known as the prototype chain.
When you access a property or method on an object, JavaScript will
first look for it on the object itself. If it’s not found, it will
traverse up the prototype chain until it finds the property or reaches
the top-level object. It will often end in Object.prototype
, which has a null
prototype, denoting the end of the chain.
A crucial difference between classical and prototypal OOP is that we can’t dynamically manipulate a class definition once an object is created. But with JavaScript prototypes, we can add, delete, or change methods and properties from the prototype, affecting the objects down the chain.
“Objects inherit from objects. What could be more object-oriented than that?”
— Douglas Crockford
What’s The Difference In JavaScript? Spoiler: None
So, on paper, the difference is simple. In classical OOP, we instantiate objects from a class, and a class can inherit methods and properties from another class. In prototypal OOP, objects can inherit properties and methods from other objects through their prototype.
However, in JavaScript, there is not a single difference beyond syntax. Can you spot the difference between the following two code excerpts?
There is no difference, and JavaScript will execute the same code, but the latter example is honest about what JavaScript is doing under the hood, while the former hides it behind syntactic sugar.
Do I have a problem with the classical approach? Yes and no. An argument can be made that the classical syntax improves readability by having all the code related to the class inside a block scope. On the other hand, it’s misleading and has led thousands of developers to believe that JavaScript has true classes when a class in JavaScript is no different from any other function object.
My biggest issue isn’t pretending that true classes exist but rather that prototypes don’t.
“
Consider the following code:
Wait,
did we just access the class prototype? Yes, because classes don’t
exist! They are merely functions returning an object (called constructor
functions), and, inevitably, they have a prototype which means we can
access its .prototype
property.
It almost looks like JavaScript tries to hide its prototypes. But why?
There Are Clues In JavaScript’s History #
In May 1995, Netscape involved JavaScript creator Brendan Eich in a project to implement a scripting language into the Netscape browser. The main idea was to implement the Scheme language into the browser due to its minimal approach. The plan changed when Netscape closed a deal with Sun Microsystems, creators of Java, to implement Java on the web. Soon enough, Brendan Eich and Sun Microsystems founder Bill Joy saw the need for a new language. A language that was approachable for people whose main focus wasn’t only programming. A language both for a designer trying to make a website and for an experienced developer coming from Java.
With this goal in mind, JavaScript was created in 10 days of intense work under the early name of Mocha. It would be changed to LiveScript to market it as a script executing “live” in the browser but in December 1995, it would ultimately be named JavaScript to be marketed along with Java. This deal with Sun Microsystems forced Brendan to accommodate his prototype-based language to Java. According to Brendan Eich, JavaScript was treated as the “sidekick language to Java” and was greatly underfunded in comparison with the Java team:
“I was thinking the whole time, what should the language be like? Should it be easy to use? Might the syntax even be more like natural language? [...] Well, I’d like to do that, but my management said, “Make it look like Java.”
Eich’s idea for JavaScript was to implement Scheme first-class functions — a feature that would allow callbacks for user events — and OOP based on prototypes from Self. He’s expressed this before on his blog:
“I’m not proud, but I’m happy that I chose Scheme-ish first-class functions and Self-ish prototypes as the main ingredients.”
JavaScript’s prototypal nature stayed but would specifically be obscured behind a Java facade. Prototypes likely remained in place because Eich implemented Self prototypes from the beginning and they later couldn’t be changed, only hidden. We can find a mixed explanation in an old comment on his blog:
“It is ironic that JS could not have class in 1995 because it would have rivaled Java. It was constrained by both time and a sidekick role.”
Either way, JavaScript became a prototype-based language and the most popular one by far.
If Only JavaScript Embraced Its Prototypes #
In the rush between the creation of JavaScript and its mass adoption, there were several other questionable design decisions surrounding prototypes. In his book, JavaScript: The Good Parts, Crockford explains the bad parts surrounding JavaScript, such as global variables and the misunderstanding around prototypes.
As you may have noticed, this article is inspired by Crockford’s book. Although I disagree with many of his opinions about JavaScript’s bad parts, it’s important to note the book was published in 2008 when ECMAScript 4 (ES4) was the stable version of JavaScript. Many years have passed since its publication, and JavaScript has significantly changed in that time. The following are features that I think could have been saved from the language if only JavaScript had embraced its prototypes.
The this
Value In Different Contexts #
The this
keyword is another one of the things JavaScript added to look like Java. In Java, and classical OOP in general, this
refers to the current instance on which the method or constructor is
being invoked, just that. However, in JavaScript, we didn’t have class
syntax until ES6 but still inherited the this
keyword. My problem with this
is it can be four different things depending on where is invoked!
1. this
In The Function Invocation Pattern #
When this
is invoked inside a function call, it will be bound to the global
object. It will also be bound to the global object if it’s invoked from
the global scope.
In strict mode and through the function invocation pattern, this
will be undefined
.
2. this
In The Method Invocation Pattern #
If we reference a function as an object’s property, this
will be bound to its parent object.
Arrow functions do not have their own this
, but instead, they inherit this
from their parent scope at creation.
In this case, this
was bound to the global object instead of dog
, hence this.name
is undefined
.
3. The Constructor Invocation Pattern #
If we invoke a function with the new
prefix, a new empty object will be created, and this
will be bound to that object.
We could also employ this
from the function’s prototype to access the object’s properties, which could give us a more valid reason to use it.
4. The apply
Invocation Pattern #
Lastly, each function inherits an apply
method from the function prototype that takes two parameters. The first parameter is the value that will be bound to this
inside the function, and the second is an array that will be used as the function parameters.
As you can see, this
can be almost anything and shouldn’t be in JavaScript in the first place. Approaches like using bind()
are solutions to a problem that shouldn’t even exist. Fortunately, this
is completely avoidable in modern JavaScript, and you can save yourself
several headaches if you learn how to dodge it; an advantage that ES6
class users can’t enjoy.
Crockford has a nice anecdote on the topic from his book:
“This is a demonstrative pronoun. Just having this
in the language makes the language harder to talk about. It is like pair programming with Abbott and Costello.”
“But if we want to create a function constructor, we will need to use this
.” Not necessarily! In the following example, we can make a function constructor that doesn’t use this
or new
to work.
We just created a function constructor without using this
or new
! And it comes with a straightforward syntax. A downside you could see is that objects created from counterConstructor
won’t have access to its prototype, so we can’t add methods or properties from counterConstructor.prototype
.
But do we need this? Of course, we will need to reuse our code, but there are better approaches that we will see later.
The new
Prefix #
In JavaScript: The Good Parts, Crockford argues that we shouldn’t use the new
prefix simply because there is no guarantee that we will remember to
use it in the intended functions. I think that it’s an easy-to-spot
mistake and also avoidable by capitalizing the constructor functions you
intend to use with new
. And nowadays, linters will warn us when we call a capitalized function without new
, or vice-versa.
A better argument is simply that using new
forces us to use this
inside our constructor functions or “classes,” and as we saw earlier, we are better off avoiding this
in the first place.
The Multiple Ways To Access Prototypes #
For the historical reasons we already reviewed, we can understand why JavaScript doesn’t embrace its prototypes. By extension, we don’t have tools to mingle with prototypes as straightforward as we would want, but rather devious attempts to manipulate the prototype chain. Things get worse when across documentation, we can read different jargon around prototypes.
The Difference Between [[Prototype]]
, __proto__
, And .prototype
#
To make the reading experience more pleasant, let’s go over the differences between these terms.
[[Prototype]]
is an internal property that holds a reference to the object’s prototype. It’s enclosed in double square brackets, which means it typically cannot be accessed using normal notation.__proto__
can refer to two possible properties:- It can refer to a property from any
Object.prototype
object that exposes the hidden[[Prototype]]
property. It’s deprecated and ill-performing. - It can refer to an optional property we can add when creating an object literal. The object’s prototype will point to the value we give it.
- It can refer to a property from any
.prototype
is a property exclusive to functions or classes (excluding arrow functions). When invoked using thenew
prefix, the instantiated object’s prototype will point to the function’s.prototype
.
We can now see all the ways we can modify prototypes in JavaScript. After reviewing, we will notice they all fall short in at least some aspect.
Using The __proto__
Literal Property At Initialization #
When creating a JavaScript object using object literals, we can add a __proto__
property. The created object will point its [[Prototoype]]
to the value given in __proto__
.
In a prior example, objects created from our function constructor
didn’t have access to the constructor prototype. We can use the __proto__
property at initialization to change this without using this
or new
.
The
advantage of linking the new object’s prototype to the function
constructor would be that we can extend its methods from the constructor
prototype. But what good would it be if we needed to use this
again?
We didn’t even modify the count
internal value but instead printed it double. So, a setter method would
be necessary to manipulate its state from outside the initial function
constructor declaration. However, we are over-complicating our code
since we could have simply added a double
method inside our function.
Using __proto__
is overkill in practice.
It’s vital to note that __proto__
must only be used when initializing a new object through an object literal. Using the __proto__
accessor in Object.prototype.__proto__
will change the object’s [[Prototoype]]
after initialization, disrupting lots of optimizations done under the hood by JavaScript engines. That’s why Object.prototype.__proto__
is ill-performant and deprecated.
Object.create()
#
Object.create()
returns a new object whose [[Prototype]]
will be the first argument of the function. It also has a second
argument that lets you define additional properties to the new objects.
However, it’s more flexible and readable to create an object using an
object literal. Hence, its only practical use would be to create an
object without a prototype using Object.create(null)
since all objects created using object literals are automatically linked to Object.prototype
.
Object.setPrototypeOf()
#
Object.setPrototypeOf()
takes two objects as arguments and will mutate the prototype chain from
the former argument to the latter. As we saw earlier, switching an
object’s prototype after initialization is ill-performing, so avoid it
at all costs.
Encapsulation And Private Classes #
My last argument against classes is the lack of privacy and encapsulation. Take, for example, the following class syntax:
We don’t have any privacy! All properties are public. We can try to mitigate this with closures:
Oops, now this.name
is undefined
outside the constructor’s scope. We have to change this.name
to this.getName()
so it can work properly.
This is with only one argument, so you can imagine how unnecessarily repetitive our code would be the more arguments we add. Besides, we can still modify our object methods:
We can save and implement better privacy if we use our own function constructors and even make our methods immutable using Object.freeze()
!
And trying to modify the object’s methods will fail silently.
And yes, I am aware of the recent proposal for private class fields. But do we really need even more new syntax when we could accomplish the same using custom constructor functions and closures?
So, Classes Or Prototypes In JavaScript? #
In Crockford’s more recent book, How JavaScript Works (PDF), we can see a better option than using Prototypes or Classes for code reuse: Composition!
“
As Crockford says in his more recent book:
“[I]nstead of same as except we can get a little bit of this and a little bit of that.”
— Douglas Crockford, How JavaScript Works
Instead of a function constructor or class inheriting from another, we can have a set of constructors and combine them when needed to create a specialized object.
Without the need for this
and new
and classes or prototypes, we achieve a reusable function constructor with full privacy and encapsulation.
Conclusion #
Yes, JavaScript was made in 10 days in a rush; yes, it was tainted by marketing; and yes, it has a long set of useless and dangerous parts. Yet is a beautiful language and fuels a lot of the innovation happening in web development today, so it clearly has done something good!
“
Unfortunately, this conscious decision to stick to the good parts isn’t exclusive to JavaScript OOP since, between the rush into existence, the language brought a lot of other dubious features that we are better off not using. Maybe we can tackle them in a future article, but in the meantime, we will have to acknowledge their presence and make the conscious decision to keep learning and understanding the language to know which parts to use and which parts to ignore.
References #
- Brendan Eich Personal Website (N.D.)
- JavaScript: The Good Parts, Douglas Crockford (O’Reilly, 2008)
- How JavaScript Works, Douglas Crockford (Virgule-Solidus, 2018)
- Douglas Crockford Personal Website (N.D.)
- Brendan Eich’s Interview Conducted by Eric Knorr
- Brendan Eich’s Interview Conducted by Lex Fridman
- Mozilla Foundation Documentation (N.D.)
No comments:
Post a Comment