Classes

Prefer ES2015/ES6 classes over ES5 plain functions

It’s very difficult to get readable class inheritance, construction, and method
definitions for classical ES5 classes. If you need inheritance (and be aware
that you might not), then prefer ES2015/ES6 classes. However, prefer small functions over
classes until you find yourself needing larger and more complex objects.

Bad:

  1. const Animal = function(age) {
  2. if (!(this instanceof Animal)) {
  3. throw new Error('Instantiate Animal with `new`');
  4. }
  5. this.age = age;
  6. };
  7. Animal.prototype.move = function move() {};
  8. const Mammal = function(age, furColor) {
  9. if (!(this instanceof Mammal)) {
  10. throw new Error('Instantiate Mammal with `new`');
  11. }
  12. Animal.call(this, age);
  13. this.furColor = furColor;
  14. };
  15. Mammal.prototype = Object.create(Animal.prototype);
  16. Mammal.prototype.constructor = Mammal;
  17. Mammal.prototype.liveBirth = function liveBirth() {};
  18. const Human = function(age, furColor, languageSpoken) {
  19. if (!(this instanceof Human)) {
  20. throw new Error('Instantiate Human with `new`');
  21. }
  22. Mammal.call(this, age, furColor);
  23. this.languageSpoken = languageSpoken;
  24. };
  25. Human.prototype = Object.create(Mammal.prototype);
  26. Human.prototype.constructor = Human;
  27. Human.prototype.speak = function speak() {};

Good:

  1. class Animal {
  2. constructor(age) {
  3. this.age = age;
  4. }
  5. move() { /* ... */ }
  6. }
  7. class Mammal extends Animal {
  8. constructor(age, furColor) {
  9. super(age);
  10. this.furColor = furColor;
  11. }
  12. liveBirth() { /* ... */ }
  13. }
  14. class Human extends Mammal {
  15. constructor(age, furColor, languageSpoken) {
  16. super(age, furColor);
  17. this.languageSpoken = languageSpoken;
  18. }
  19. speak() { /* ... */ }
  20. }

Use method chaining

This pattern is very useful in JavaScript and you see it in many libraries such
as jQuery and Lodash. It allows your code to be expressive, and less verbose.
For that reason, I say, use method chaining and take a look at how clean your code
will be. In your class functions, simply return this at the end of every function,
and you can chain further class methods onto it.

Bad:

  1. class Car {
  2. constructor(make, model, color) {
  3. this.make = make;
  4. this.model = model;
  5. this.color = color;
  6. }
  7. setMake(make) {
  8. this.make = make;
  9. }
  10. setModel(model) {
  11. this.model = model;
  12. }
  13. setColor(color) {
  14. this.color = color;
  15. }
  16. save() {
  17. console.log(this.make, this.model, this.color);
  18. }
  19. }
  20. const car = new Car('Ford','F-150','red');
  21. car.setColor('pink');
  22. car.save();

Good:

  1. class Car {
  2. constructor(make, model, color) {
  3. this.make = make;
  4. this.model = model;
  5. this.color = color;
  6. }
  7. setMake(make) {
  8. this.make = make;
  9. // NOTE: Returning this for chaining
  10. return this;
  11. }
  12. setModel(model) {
  13. this.model = model;
  14. // NOTE: Returning this for chaining
  15. return this;
  16. }
  17. setColor(color) {
  18. this.color = color;
  19. // NOTE: Returning this for chaining
  20. return this;
  21. }
  22. save() {
  23. console.log(this.make, this.model, this.color);
  24. // NOTE: Returning this for chaining
  25. return this;
  26. }
  27. }
  28. const car = new Car('Ford','F-150','red')
  29. .setColor('pink')
  30. .save();

Prefer composition over inheritance

As stated famously in Design Patterns by the Gang of Four,
you should prefer composition over inheritance where you can. There are lots of
good reasons to use inheritance and lots of good reasons to use composition.
The main point for this maxim is that if your mind instinctively goes for
inheritance, try to think if composition could model your problem better. In some
cases it can.

You might be wondering then, “when should I use inheritance?” It
depends on your problem at hand, but this is a decent list of when inheritance
makes more sense than composition:

  1. Your inheritance represents an “is-a” relationship and not a “has-a”
    relationship (Human->Animal vs. User->UserDetails).
  2. You can reuse code from the base classes (Humans can move like all animals).
  3. You want to make global changes to derived classes by changing a base class.
    (Change the caloric expenditure of all animals when they move).

Bad:

  1. class Employee {
  2. constructor(name, email) {
  3. this.name = name;
  4. this.email = email;
  5. }
  6. // ...
  7. }
  8. // Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
  9. class EmployeeTaxData extends Employee {
  10. constructor(ssn, salary) {
  11. super();
  12. this.ssn = ssn;
  13. this.salary = salary;
  14. }
  15. // ...
  16. }

Good:

  1. class EmployeeTaxData {
  2. constructor(ssn, salary) {
  3. this.ssn = ssn;
  4. this.salary = salary;
  5. }
  6. // ...
  7. }
  8. class Employee {
  9. constructor(name, email) {
  10. this.name = name;
  11. this.email = email;
  12. }
  13. setTaxData(ssn, salary) {
  14. this.taxData = new EmployeeTaxData(ssn, salary);
  15. }
  16. // ...
  17. }