JavaScript does not need classes
Join the DZone community and get the full member experience.
Join For FreeOne frequently encounters the opinion that JavaScript’s prototypal inheritance is too complicated and that it needs classes to be more user-friendly. This post argues that this opinion is wrong.
Executive summary: The core of JavaScript’s prototypal inheritance is very simple, it is just obscured by an awkward way of creating instances via constructors. Therefore, we don’t need classes, we just need to fix constructors. Read on for three ways of doing so.
Prototypal inheritance is simple
In one way, JavaScript is clearly superior to most class-based languages – you can directly create objects:
var jane = { name: "Jane", describe: function() { return "Person called "+this.name; } }; console.log(jane.describe()); // Person called Janejane is an object which has been created via an object literal. name and describe are properties. describe is a property whose value is a function. Such function-valued properties are called methods. An object literal looks like a dictionary (map), but it is a real object. In most class-based languages, you need a class to create one. Hence the singleton pattern.
The core idea of prototypal inheritance is incredibly simple, much simpler than classes. What makes JavaScript so complicated is that this core idea is obscured by trying to make the creation of instances (of a given type) look like Java. The core idea of prototypal inheritance is: an object can point to another object and thus make it its prototype. If a property isn’t found in the object, the search continues in the prototype (and, if it has one, its prototype, etc.). That allows one to model jane and tarzan as “instances” of PersonProto:
var PersonProto = { describe: function () { return "Person called "+this.name; }, }; var jane = { __proto__: PersonProto, name: "Jane", }; var tarzan = { __proto__: PersonProto, name: "Tarzan", }; console.log(jane.describe()); // Person called Janejane and tarzan share the same prototype PersonProto which provides method describe() to both of them. Note how similar PersonProto is to a class.
Constructors. The default way of producing a factory for instances of PersonProto is via a constructor function (short: constructor). It is a normal function that is invoked via the new operator to produce an instance. In the following code, Person is a constructor. Person.prototype is the same object as PersonProto, it becomes the shared prototype of all instances of Person. Obviously, constructors correspond to classes in other languages.
// Constructor: set up the instance function Person(name) { this.name = name; } // Prototype: shared by all instances Person.prototype.describe = function () { return "Person called "+this.name; }; var jane = new Person("Jane"); console.log(jane instanceof Person); // true console.log(jane.describe()); // Person called JaneExtending constructors. Things only become nasty once you come to extending a constructor, to creating a sub-constructor via inheritance. Let’s build Employee as a sub-constructor of Person: An employee is a person, but it additionally has a title and its describe() method works differently in that it also mentions the title.
function Employee(name, title) { Person.call(this, name); this.title = title; } Employee.prototype = Object.create(Person.prototype); Employee.prototype.constructor = Employee; Employee.prototype.describe = function () { return Person.prototype.describe.call(this) + " (" + this.title + ")"; }; var jane = new Employee("Jane", "CTO"); console.log(jane instanceof Person); // true console.log(jane instanceof Employee); // true console.log(jane.describe()); // Person called Jane (CTO)There is no doubt that that is unwieldy (details are explained here). As a result, numerous libraries for handling inheritance have been written for JavaScript. But JavaScript needs a built-in solution. The following sections present three candidates for ECMAScript.next.
Inheritance solution 1: minor language additions
If you add four minor constructs to JavaScript (as proposed for ECMAScript.next) then things become much easier:
- The inheritance operator <| (read as “is extended by”):
var Super = function () { ... } var Sub = Super <| function () { ... }
The constructor Sub extends the constructor Super. - Super property access:
super.describe()
- The extension operator .= adds properties to an object instead of replacing it:
var colorPoint = { color: "green" }; colorPoint .= { x: 33, y: 7 }
Afterwards, colorPoint has the value{ color: "green", x: 33, y: 7 }
- Shorter method syntax for object literals:
{ method(arg1, arg2) { ... } }
is an abbreviation for{ method: function (arg1, arg2) { ... } }
function Person(name) { this.name = name; } Person.prototype .= { describe() { return "Person called "+this.name; } }; var Employee = Person <| function (name, title) { super.constructor(name); this.title = title; } Employee.prototype .= { describe() { return super.describe() + " (" + this.title + ")"; } };That is already quite usable. Stopping here would be OK, but we can do a little better.
Inheritance solution 2: object exemplars
An exemplar is a factory for instances. In class-based languages,
the exemplars are classes. In current JavaScript, the exemplars are
functions (constructors, as described above). But you can in principle
also use the prototype (an object) as an exemplar. That is the core idea
of object exemplars. They profit from the language constructs that have been introduced in the previous section:
var Person = { constructor(name) { this.name = name; }, describe() { return "Person called "+this.name; } }; var Employee = Person <| { constructor(name, title) { super.constructor(name); this.title = title; }, describe() { return super.describe() + " (" + this.title + ")"; } }; var jane = new Employee("Jane", "CTO"); console.log(jane instanceof Employee); // trueFor the above to work, you only need to adapt the following things:
- The inheritance operator <| must work for objects, too. Then it sets the prototype of an object.
- new must accept objects as operands, not just functions.
- instanceof must allow objects as its right-hand side, not just functions.
- super works the same for function exemplars and object exemplars: It looks for the super-property starting in the prototype of where the current method is located.
A major disadvantage of this approach is that it is different from the currently used function exemplars: Now prototypes bear the name of the type and not constructors. But one can achieve a certain amount of compatibility, by ensuring that both the left-hand side and the right-hand side of the inheritance operator can be either a function or an object:
Constructor1 <| Constructor2 Prototype1 <| Prototype2 Constructor <| Prototype Prototype <| ConstructorThe main advantage of object exemplars is their simplicity. The creators of the programming language Self (which Eich cites as one of JavaScript’s influences) have always been aware of this simplicity. They explain it well in the paper “Organizing Programs Without Classes” by David Ungar, Craig Chambers, Bay-Wei Chang, Urs Hölzle (highly recommended, very readable).
Inheritance solution 3: class declarations
Even though the construct described here is called a class declaration,
it is not really a class: It is syntactic sugar for a constructor and
thus internally translated to a function. The syntax for class
declarations is currently being worked on. It might look something like
this:
class Person { constructor(name) { this.name = name; }, describe() { return "Person called "+this.name; } } class Employee extends Person { constructor(name, title) { super.constructor(name); this.title = title; }, describe() { return super.describe() + " (" + this.title + ")"; } }JavaScript has a long history of proposals for class-like syntactic constructs. Three current proposals are (ordered chronologically):
Naturally, Brendan Eich’s proposal will be the most influential one, but it has been informed by Ashkenas’ ideas. My proposal tries to strike a balance between the two and has been informed by Allen Wirfs-Brock’s ideas on object literals and object exemplars.
The pros and cons of class declarations are:
- Pros: They help with inheritance usability problems, especially if one is coming from a class-based language. And they are compatible with constructors, the current standard for instance factories.
- Con: They are not really true to the prototypal nature of the language – as opposed to object exemplars, which look practically the same as class declarations. Class declarations hide the complexities of constructor-based instance creation, object exemplars remove them.
Multiple inheritance
Multiple inheritance has been explicitly rejected for ECMAScript.next,
so none of the presented solutions will support it. However, there is a proposal to add it later (after ECMAScript.next), in the form of traits.
Conclusion
At its core, JavaScript’s prototypal inheritance is dead simple. I would
love to preserve this simplicity by making object exemplars the default
instance factories in JavaScript. But partially breaking backward
compatibility is a major hurdle. Hence, I would still be satisfied with
the second-best solution: class declarations. Even though these are
called classes, they are actually functions (which explains the heading
of this post).
Addendum: “JavaScript needs to be replaced”
Robert Koritnik makes a valid observation on Google+:
Well just look at the number of solutions which do nothing more but add to confusion. Javascript would really be great if it supported static types. But instead of extending Javascript it would probably be better to come up with a second language all browsers would support and would be OO and statically typed.
Answer: You are conflating three issues:
- Subtyping is currently complicated, current solutions only partially help: That problem will go away in ECMAScript.next, because it will introduce a standard mechanism for subtyping (one way or the other). That mechanism will be easy to understand – do give the options described in [this post] a chance; they look unusual to people who are mostly familiar with classes, but they are actually quite simple.
- Supporting static types: A lot of what static typing gives you can be achieved via type inference. The remainder can be added via type guards (which are on deck for ECMAScript.next.next).
- Replacing JavaScript with something better: Google is currently trying to do so with Dart. But once you get used to JavaScript, there is something missing in less dynamic languages such as Dart. Furthermore, backward compatibility is a huge argument in favor of JavaScript. Lastly, other browser vendors are not likely to switch to the Google-controlled Dart (open source or not). Starting from scratch is a typical engineering fallacy, improving what’s there usually produces better results (the “worse is better” approach). ECMAScript.next will be a great language – it removes most of JavaScript’s quirks, but keeps its nimbleness.
You can read the rest of the thread on Google+.
Related reading
- Prototypes as classes – an introduction to JavaScript inheritance [explains object exemplars in detail, under their former name “prototypes as classes”; includes a library for ECMAScript 5]
- A brief history of ECMAScript versions (including Harmony and ES.next)
- ECMAScript.next: the “TXJS” update by Eich [an overview of what’s currently planned for ECMAScript.next a.k.a. ECMAScript 6]
Source: http://www.2ality.com/2011/11/javascript-classes.html
Opinions expressed by DZone contributors are their own.
Comments