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 ==:

[] ==  0;   // -> true
 0  == "0";  // -> true
"0" == [];   // -> false

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++:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

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:

typeof Rectangle  // -> 'function'

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:

typeof "hi";  // -> 'string'
typeof 3;     // -> 'number'
typeof {};    // -> 'object'

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:

let myRectangle = {
  width: 2,
  height: 4
};

Methods can also be assigned to an object via this syntax. They are defined just like any other property:

let myRectangle = {
  width: 2,
  height: 4,
  area: function() {
    return this.height * this.width;
  }
};

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).

What's this?

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:

let obj = {
  f: function() { return this; }
};
let g = obj.f;
g();  // -> undefined

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:

g.call(obj)  // -> { f: [Function …] }

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:

g.call(Math)  // -> Object [Math] {}

To avoid doing this accidentally, you will often encounter something like the following in JavaScript code:

let h = obj.f.bind(obj)
h();  // -> { f: [Function …] }

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:

let obj = { f: () => { return this; }}

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:

function createRect(height, width) {
  return {
    width: width,
    height: height
  };
}

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:

function Rect(height, width) {
  this.height = height;
  this.width = width;
}

let r = new Rect(20, 30);
r.width;  // -> 30

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:

Object.getPrototypeOf(Rect);  // -> {}
Rect.prototype;  // -> {}
Object.getPrototypeOf(Rect) ===  Rect.prototype;  // -> false

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:

let s = Object.create(Rect.prototype);
Rect.call(s, 20, 40);

s instanceof Rect;  // -> true
s.width;  // -> 40
s.constructor;  // -> [Function: Rect]
Object.getPrototypeOf(s) ===  Rect.prototype;  // -> true

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.

JavaScript has been in widespread use since the Netscape era. Even back then, the object system already functioned as it does today.
JavaScript has been in widespread use since the Netscape era. Even back then, the object system already functioned as it does today.

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:

Rect.prototype.area = function() {
  return this.height * this.width;
}

You can now call area() on all objects that were created with new Rect(…) (or built-up manually in the same way):

r.area();  // -> 600

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:

function Shape(type) {
  this.type = type;
}

Now you can extend the Rect function with a manual call to the parent function:

function Rect(height, width) {
  Shape.call(this, "rectangle");
  this.height = height;
  this.width = width;
}

You must then link up the prototype chain correctly. This is done with the built-in function Object.setPrototypeOf():

Object.setPrototypeOf(Rect.prototype, Shape.prototype)

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.

Prototype inheritance: The object r was created using the Rect() function, which in turn calls Shape(), giving r all the properties defined by Rect() and Shape(). A chain of prototype objects is created through appropriate wiring after object creation, allowing properties such as the method area() to be inherited. In contrast to class-based inheritance, the prototypes and their chaining can be changed at any time.
Prototype inheritance: The object r was created using the Rect() function, which in turn calls Shape(), giving r all the properties defined by Rect() and Shape(). A chain of prototype objects is created through appropriate wiring after object creation, allowing properties such as the method area() to be inherited. In contrast to class-based inheritance, the prototypes and their chaining can be changed at any time.

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:

class Rectangle extends Shape {
  constructor(height, width) {
    super("rectangle");
    this.height = height;
    this.width = width;
  }

  area() {
    return this.height * this.width;
  }
}

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.

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:

// Calls as constructor
new Array(1, 2, 3);  // -> [ 1, 2, 3 ]
new Date('1983')  // -> 1983-01-01…

// Calls as functions
Array(1, 2, 3); // -> [ 1, 2, 3 ]
Date('1983')  // -> 'Tue Sep 27 2022 …

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.

Conclusion

Object orientation in JavaScript is often misunderstood. It works differently than in other languages, and this hasn’t changed even though “classes” are now available as an option. These may offer a convenient way to create objects, but they are still based on prototypes.

In addition to the functions discussed in this article, there are plenty of technical tricks for solving special problems. For example, you can modify the prototype chains at any time. One good place to start looking for more information is the MDN Web Docs. However, the overview provided in this article should be sufficient to understand typical constructions and solve general problems.