Dieser Artikel ist auch auf Deutsch verfügbar
JavaScript takes a unique approach when it comes to classes and objects. This may be unsurprising to many coders. After all, the language is well known for its many eccentricities, which can be either exciting or frustrating, depending on the context. One good example is the curious behavior of the equality operator ==
:
An empty array is equal to 0 (sure, why not?), 0 is equal to the character zero (sounds like a stretch…), but then how is the character zero not equal to the empty array?! So much for the “equality” of the operator. Such unintuitive antics could be seen as a design flaw, leading in practice to a preference for the newer type-checking comparison operator ===
. But we aren’t getting off the hook that easy since JavaScript has never parted ways with such old notions as ==
. This backward compatibility has certainly been key to the widespread adoption of the language. Code that ran in Netscape Navigator back in the day still runs today in Firefox, Chrome and countless other JavaScript engines, both front and back end. But this compatibility also means that you still have to know the old tricks because even modern code can’t get by without them.
Class or function?
This is especially true when it comes to the JavaScript object system. Many coders rejoiced when ECMAScript 6 introduced the class keyword in 2015. We finally had a convenient way to define classes, and JavaScript was able to join the ranks of longstanding members of the object-oriented community like Java and C++:
Never again must we create objects via functions and implement inheritance with “prototypes”, those bizarre constructions unknown in practically any language except JavaScript. Not so fast. It turns out that classes in JavaScript are just an add-on that does not eliminate, deprecate or cover up the old paradigm:
The supposed class Rectangle
reveals itself to be a function. What is going on here?
In contrast to a language like TypeScript, JavaScript has no compiler that checks the static types of functions or variables. It operates instead with two categories of values that can exist at runtime. The first is scalar values (also called primitives): String, Number, BigInt, Boolean, Undefined, Symbol and Null. Such values are immutable. For example, it is not possible to turn a character into a string at some point without creating an entirely new variable.
The second category – in other words, everything else – consists of objects. The typeof
operator determines which type of value we are currently dealing with:
In JavaScript, therefore, objects are simply collections of properties with values that can change. The value could have any type at all, meaning that objects can even be nested. Functions are also objects – in this case, objects that can be called.
Literals
Unlike many other programming languages, JavaScript supports the creation of actual objects by simply defining their properties in the code. To define an object representing the dimensions of a rectangle, you don’t have to code up any classes or constructors. All you have to do is place an object literal in curly braces:
Methods can also be assigned to an object via this syntax. They are defined just like any other property:
You can call the method as in any other language (myRectangle.width
or myRectangle.area()
), but the exact meaning of this
can pose some difficulties (see box).
Hardly any keyword in JavaScript is as misunderstood as this
. Like in other languages, it is used within a method to reference the parent object. But JavaScript doesn’t actually have methods, only functions.
Some functions are bound to an object as a property, allowing you to call them with dot or bracket notation (obj.f()
or obj["f"]()
). Even this article speaks in terms of “methods” even though – strictly speaking – these are just normal functions and not object methods. The binding to the object is dynamic.
As a result, the keyword this
functions differently in JavaScript: When you call a function f
, the JavaScript engine dynamically binds this
to the specific object with which f
was called. When you see the code obj.f()
, it is this line itself – not the location where f
is found – which defines that this
refers to obj
.
It follows from this dynamic assignment that this
is not always defined in JavaScript:
g = obj.f
gives the function a new name, independent of obj
. When you simply call g()
, there is no object to assign it to, meaning that it remains undefined. (At least in JavaScript’s “strict mode”. In the normal, “sloppy” mode, this
returns the global object in such cases.)
Not only can you break the reference of this
, you can also redefine it:
Instead of call()
, you can also invoke apply()
. The only difference is whether you pass the actual parameters of the function (assuming any exist) separately or as an array. Of course, both methods allow you to call a function from any object:
To avoid doing this accidentally, you will often encounter something like the following in JavaScript code:
The method bind()
creates a copy of its function (here f
), whose this
is specifically bound to the passed object (obj
). This allows you to call h
even without an object and still get obj
.
this
has a number of other interesting aspects in JavaScript. Since version 6, for example, JavaScript has supported another way to define functions:
With this “fat arrow” syntax, this
is not bound to the function. It acquires its value from the surrounding context instead. This and other details are explained in the Mozilla JavaScript documentation.
Templates
Object literals are useful when you want to define a specific individual object. But what if you need some kind of template to generate many similar functions? This is exactly why most programming languages have classes, but JavaScript shows that it can be done another way:
To generate objects from a template, you simply define a function that takes parameters and returns an object defined as a literal.
However, the originating function will not be apparent in the result. Code to which such an object is passed cannot see that it comes from createRect()
, represents a rectangle and consequently has the properties width
and height
.
In traditional JavaScript, this problem is solved by writing and calling functions in a special way:
This might not seem much different at first glance, but let’s take a closer look: Instead of explicitly creating an object, we have simply assigned the desired properties to this
. There is no longer an explicit return value. Plus, we call the function with the new
operator. Overall, the syntax seems very similar to class definitions and object constructions in other programming languages. The same naming convention of beginning with an uppercase letter is even used.
But how does this type of object creation enable inheritance? How does it identify where an object comes from? And how does such a class-like function even work?
Prototypes
For one thing, every object contains an internal reference to a prototype object. The prototype is itself also an object. In other words, it also references its own prototype object, and so on. The prototype chain ends in a reference to null
. The built-in method Object.getPrototypeOf(obj)
can be used to trace the chain.
For another thing, functions in JavaScript are a special kind of object, meaning that they also have a prototype object. Moreover, JavaScript attaches still another object to practically every function by assigning it to the property prototype
. There are a few exceptions to this, but they are beyond the scope of this article.
In the case of Rect, both objects may be empty, but they are not identical:
This is because a property declared in Rect.prototype
is not available in Rect
itself but only in objects created with new Rect
. In this sense, Rect.prototype
serves as a kind of “template” for objects produced by new Rect
. As you can see, we are dealing with two different but quite similar concepts.
Two other things also happen internally when the new
keyword is placed before the call to Rect
. First, the built-in function Object.create()
is called with the object in Rect.prototype
as parameter. This call creates a new object, and this object receives the object in Rect.prototype
as its prototype. Second, the new object receives the property constructor
with a reference back to Rect
. This is how the engine keeps track of which function constructed the object.
In the second step, the creating function (in the example: Rect()
) is called, where this
refers to the new object just created so that it can be populated with properties. The new object is now finished. The internal steps carried out by new
can even be followed manually:
In practice, it makes no sense to reinvent new
, but it helps to understand how JavaScript works. See the box above for an explanation of what call()
does. The instanceof
operator essentially checks the same thing as the last line. It does not restrict itself to the first prototype object, however. It travels down the entire prototype chain to find a match. In theory, you could bypass this check, but that is a topic for another article.
Methods and inheritance
In JavaScript, the prototype chain replaces the class hierarchy found in other languages. When you try to access a property that an object doesn’t have, the engine will automatically look for it in the prototype object. If that fails, it checks the prototype of the prototype, and so on. Only if it reaches the end of the chain without finding the property is the object undefined
.
This allows you to assign shared properties to all rectangles, such as a method for calculating area:
You can now call area()
on all objects that were created with new Rect(…)
(or built-up manually in the same way):
This also applies to objects that were created before area()
was even defined since the prototype chain is traversed upon every single access. You could think of prototypes as modifiable blueprints for entire groups of objects, which can be assigned shared behaviors in this way.
The inheritance of properties also functions via the prototype chain. Consider, for instance, a “class” for general shapes, not just rectangles:
Now you can extend the Rect function with a manual call to the parent function:
You must then link up the prototype chain correctly. This is done with the built-in function Object.setPrototypeOf()
:
The prototype for rectangles (stored in the prototype
property of Rect
) then receives as its prototype the prototype object for general shapes (stored in Shape.prototype
).
A rectangle r
created with new Rect(…)
receives Rect.prototype
as its prototype object. It is attached to the front of the chain that is traversed by the interpreter upon every call. This allows you to use r
to call functions defined directly as properties of r
, functions defined in Rect.prototype
, and functions provided by Shape.prototype
.
The figure above illustrates the various objects and their relationships. Even this simple example makes it clear why classes were so eagerly awaited in JavaScript. Prototype-based inheritance is a very powerful tool, but it is complicated and prone to errors.
Classes in JavaScript
JavaScript now also supports classes and class-based inheritance. The syntax is considerably simpler and clearer, especially since it resembles that of other programming languages:
But don’t let yourself be fooled by how similar this code snippet appears to other commonly used languages. Classes just offer a more palatable syntax. Underneath the hood, you will still find the same functions and chained prototype objects.
- The class
Rectangle
is actually a (special) function whose code is specified in theconstructor
. In contrast to the classic syntax variant, however, JavaScript does not allow this function to be called withoutnew
. -
extends
links the prototype objects inRectangle.prototype
andShape.prototype
in the same way as callingObject.setPrototypeOf()
would do. (The keyword also links up the prototypes of the classes themselves so that static properties are also inherited:Object.setPrototypeOf(Rectangle, Shape)
.) -
super(…)
provides a more conventional way to call the constructor of the parent class thanShape.call(this, …)
. -
area()
and other functions specified in this way are automatically defined forRectangle.prototype
without the need to directly modify that object.
There is no reason to avoid the class syntax when coding in JavaScript. In fact, there are many good reasons to use it. Because it is considerably easier to read, for instance, and because it offers access to additional features, such as private properties. But it is important not to forget or ignore the system underlying the class syntax in JavaScript. Lots of existing JavaScript code has not yet been ported to classes. Such code may use JavaScript’s system in ways that simply can’t be implemented with classes. For example, plenty of functions may work even without new
, but they might not behave the same as with the keyword:
Array
returns arrays even when it is called as a function. Date
also works as a function, but it then ignores all parameters and returns a string with the current date rather than a date object.