What benefits does ES2015 (ES6) `class` syntax provide?

40.2k Views Asked by At

I have many question about ES6 classes.

What's the benefit of using class syntax? I read that public/private/static will be part of ES7, is that a reason?

Moreover, is class a different kind of OOP or it still JavaScript's prototypical inheritance? Can I modify it using .prototype ? Or is it just the same object but two different ways to declare it.

Are there speed benefits? Maybe it's easier to maintain/understand if you have a big application like big app?

4

There are 4 best solutions below

11
On BEST ANSWER

The new class syntax is mostly, though not entirely, syntactic sugar (but, you know, the good kind of sugar). It markedly simplifies writing constructor functions and the objects they assign as prototypes to the objects they create, especially when setting up inheritance hierarchies, which was error-prone with the ES5 syntax. But unlike the old way, class syntax also enables super.example() for supercalls (which are notoriously hard to do the old way) as well as property declarations, private fields, and private methods (including static ones).

(Sometimes people say you have to use class syntax if you want to subclass Error or Array [which couldn't be properly subclassed in ES5]. That's not true, you can use a different ES2015 feature, Reflect.construct [spec, MDN], if you don't want to use class syntax.¹)

Moreover, is class a different kind of OOP or it still JavaScript's prototypical inheritance?

It's the same prototypical inheritance we've always had, just with cleaner, more convenient, and less error-prone syntax if you like using constructor functions (new Foo, etc.), plus some added features.

Can I modify it using .prototype?

Yes, you can still modify the prototype object on the class's constructor once you've created the class. E.g., this is perfectly legal:

class Foo {
    constructor(name) {
        this.name = name;
    }
    
    test1() {
        console.log("test1: name = " + this.name);
    }
}
Foo.prototype.test2 = function() {
    console.log("test2: name = " + this.name);
};

Are there speed benefits?

By providing a specific idiom for this, I suppose it's possible that the engine may be able to do a better job optimizing. But they're awfully good at optimizing already, I wouldn't expect a significant difference. One thing in particular about class syntax is that if you use property declarations, you can minimize the number of shape changes an object goes through when being constructed, which can make interpreting and later compiling the code a bit faster. But again, it's not going to be big.

What benefits does ES2015 (ES6) class syntax provide?

Briefly: If you don't use constructor functions in the first place, preferring Object.create or similar, class isn't useful to you.

If you do use constructor functions, there are some benefits to class:

  • The syntax is simpler and less error-prone.

  • It's much easier (and again, less error-prone) to set up inheritance hierarchies using the new syntax than with the old.

  • class defends you from the common error of failing to use new with the constructor function (by having the constructor throw an exception).

  • Calling the parent prototype's version of a method is much simpler with the new syntax than the old (super.method() instead of ParentConstructor.prototype.method.call(this) or Object.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this)).

  • Property declarations can make the shape of the instances being created clearer, separating it from the constructor logic.

  • You can use private fields and methods (both instance and static) with class syntax, and not with ES5 syntax.

Here's a syntax comparison (without private members) for a hierarchy:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        // ...
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        // ...use `result` for something...
        return result;
    }

    managerMethod() {
        // ...
    }
}

Example:

// ***ES2015+**
class Person {
    constructor(first, last) {
        this.first = first;
        this.last = last;
    }

    personMethod() {
        return `Result from personMethod: this.first = ${this.first}, this.last = ${this.last}`;
    }
}

class Employee extends Person {
    constructor(first, last, position) {
        super(first, last);
        this.position = position;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.position = ${this.position}`;
    }

    employeeMethod() {
        // ...
    }
}

class Manager extends Employee {
    constructor(first, last, position, department) {
        super(first, last, position);
        this.department = department;
    }

    personMethod() {
        const result = super.personMethod();
        return result + `, this.department = ${this.department}`;
    }

    managerMethod() {
        // ...
    }
}

const m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

vs.

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    // ...
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    // ...use `result` for something...
    return result;
};
Manager.prototype.managerMethod = function() {
    // ...
};

Live Example:

// **ES5**
var Person = function(first, last) {
    if (!(this instanceof Person)) {
        throw new Error("Person is a constructor function, use new with it");
    }
    this.first = first;
    this.last = last;
};

Person.prototype.personMethod = function() {
    return "Result from personMethod: this.first = " + this.first + ", this.last = " + this.last;
};

var Employee = function(first, last, position) {
    if (!(this instanceof Employee)) {
        throw new Error("Employee is a constructor function, use new with it");
    }
    Person.call(this, first, last);
    this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.personMethod = function() {
    var result = Person.prototype.personMethod.call(this);
    return result + ", this.position = " + this.position;
};
Employee.prototype.employeeMethod = function() {
    // ...
};

var Manager = function(first, last, position, department) {
    if (!(this instanceof Manager)) {
        throw new Error("Manager is a constructor function, use new with it");
    }
    Employee.call(this, first, last, position);
    this.department = department;
};
Manager.prototype = Object.create(Employee.prototype);
Manager.prototype.constructor = Manager;
Manager.prototype.personMethod = function() {
    var result = Employee.prototype.personMethod.call(this);
    return result + ", this.department = " + this.department;
};
Manager.prototype.managerMethod = function() {
    // ...
};        

var m = new Manager("Joe", "Bloggs", "Special Projects Manager", "Covert Ops");
console.log(m.personMethod());

As you can see, there's lots of repeated and verbose stuff there which is easy to get wrong and boring to retype (I used to use a script for it, back in the day, before class came along).

I should note that in the ES2015 code, the Person function is the prototype of the Employee function, but that's not true in the ES5 code. In ES5, there's no way to do that; all functions use Function.prototype as their prototype. Some environments supported a __proto__ pseudo-property that might have allowed changing that, though. In those environments, you could do this:

Employee.__proto__ = Person; // Was non-standard in ES5

If for some reason you wanted to do this with function syntax instead of class in an ES2015+ environment, you'd use the standard Object.setPrototypeOf instead:

Object.setPrototypeOf(Employee, Person); // Standard ES2015+

But I can't see any strong motivation for using the old syntax in an ES2015+ environment (other than to experiment with understanding how the plumbing works).

(ES2015 also defines a __proto__ accessor property that is a wrapper for Object.setPrototypeOf and Object.getPrototypeOf so that code in those non-standard environments becomes standard, but it's only defined for legacy code and is "normative optional" meaning an environment is not required to provide it.)


¹ Here's how you'd use Reflect.construct to subclass Error (for instance) if you didn't want to use class syntax:

// Creating an Error subclass:
function MyError(...args) {
  return Reflect.construct(Error, args, this.constructor);
}
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;
MyError.prototype.myMethod = function() {
  console.log(this.message);
};

// Example use:
function outer() {
  function inner() {
    const e = new MyError("foo");
    console.log("Callng e.myMethod():");
    e.myMethod();
    console.log(`e instanceof MyError? ${e instanceof MyError}`);
    console.log(`e instanceof Error? ${e instanceof Error}`);
    throw e;
  }
  inner();
}
outer();
.as-console-wrapper {
  max-height: 100% !important;
}

0
On

ES6 classes are syntactic sugar for the prototypical class system we use today. They make your code more concise and self-documenting, which is reason enough to use them (in my opinion).

Using Babel to transpile this ES6 class:

class Foo {
  constructor(bar) {
    this._bar = bar;
  }

  getBar() {
    return this._bar;
  }
}

will give you something like:

var Foo = (function () {
  function Foo(bar) {    
    this._bar = bar;
  }

  Foo.prototype.getBar = function () {
    return this._bar;
  }

  return Foo;
})();

The second version isn't much more complicated, it is more code to maintain. When you get inheritance involved, those patterns become even more complicated.

Because the classes compile down to the same prototypical patterns we've been using, you can do the same prototype manipulation on them. That includes adding methods and the like at runtime, accessing methods on Foo.prototype.getBar, etc.

There is some basic support for privacy in ES6 today, although it's based on not exporting the objects you don't want accessible. For example, you can:

const BAR_NAME = 'bar';

export default class Foo {
  static get name() {
    return BAR_NAME;
  }
}

and BAR_NAME will not be available for other modules to reference directly.

A lot of libraries have tried to support or solve this, like Backbone with their extends helper that takes an unvalidated hash of method-like functions and properties, but there's no consist system for exposing prototypical inheritance that doesn't involve mucking around with the prototype.

As JS code becomes more complicated and codebases larger, we've started to evolve a lot of patterns to handle things like inheritance and modules. The IIFE used to create a private scope for modules has a lot of braces and parens; missing one of those can result in a valid script that does something entirely different (skipping the semicolon after a module can pass the next module to it as a parameter, which is rarely good).

tl;dr: it's sugar for what we already do and makes your intent clear in code.

0
On

ES6 classes are syntactical sugar on top of constructors that were already there in javascript. Having syntax and semantics which are similar to what we have in other languages, allows programmers to shift from other languages and understand syntax more easily. Below is how a class created as per es6 classes:

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

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

In other languages like java and c++, You might have seen the concept of static properties and methods. This is how you will do it now in javascript with new syntax:

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

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

  static side() {
    return "This shape have four sides";
  }
}

And this is how you declare private fields with this new syntax:

class Base {
  #privateField1 = 26;
  #privateField2;

  constructor(val) {
    this.#privateField2 = val;
  }

  getprivateField2() {
    return this.#privateField2;
  }
}

let main = () => {
  let obj = new Base(27);
  console.log(`Private field 2 using getter : ${obj.getprivateField2()}`);
};

Keep in mind that all of this underneath is still constructors. This new just makes it easy to relate to how programmers've been doing things in other languages.

0
On

The class syntax is more than just syntactic sugar in one key respect: private variables.

I'll grab a bit of js from one of the other examples.

var Foo = (function () {
  function Foo(bar) {    
    this._bar = bar;
  }

  Foo.prototype.getBar = function () {
    return this._bar;
  }

  return Foo;
})();

//similar to
class Foo {
  constructor(bar) {
    this._bar = bar;
  }

  getBar() {
    return this._bar;
  }
}

Note the _bar. This is the idiom denoting that something is private while making it public. When it was invented there was no way to have a true private variable, not without hiding it in a closure. The trouble with hiding it in a closure is that it made the variables truly private, so private they couldn't even be seen when the object was logged to the console. And that is a MAJOR drawback. Making the internal state hard to see hinders development.

A long time ago I dropped the underscore prefix for private vars and just started making all vars public. I had zero issue with this approach because as a matter of discipline I always treat all vars as private. They can only be accessed via a method of the owning object to preserve the benefits of encapsulation. Making them public made them easy to interrogate during an interactive development/debugging session.

That is where Class privates save the day.

class Foo {
  #bar;

  constructor(bar) {
    this.#bar = bar;
  }

  getBar() {
    return #.bar;
  }
}

These vars can be readily observed by the developer from the console. But they are actually private, not faux private. So encapsulation is preserved and so is developer transparency. And that's more than just syntactic sugar. You don't get that with the other approaches.