JavaScript is an easy to learn programming language. It’s easy to write programs that run and does something. However, it’s hard to account for all the uses cases and write robust JavaScript code.
In this article, we’ll look at how to define and use classes in the best way.
Constructors
Constructors are optional in classes.
Subclass constructor must call super
before setting any fields or referencing this
.
Fields
Object fields should be set with values.
Fields should never be set on a class’s prototype
.
For instance, we write:
class Foo {
constructor() {
this.bar_ = getBar();
this.baz = getBaz();
}
}
Properties should never be added or remove from an instance after the constructor is finished.
This will impede any optimization that may be done.
Rather, we should set properties that are set to undefined
if we don’t want the value anymore.
Computed Properties
We should only use computed properties in our key is a symbol.
Also, we shouldn’t use built-in symbols unless we know we need to use them.
For instance, if we’re creating a class to create iterable objects, then we need to use Symbol.iterator
.
Static Methods
We should add private module functions rather than static methods.
For instance, instead of writing:
class Bar {
static foo() {}
//...
}
We write:
const foo = () => {
//...
}
class Bar {
//...
}
Old-Style Class Declarations
ES6 classes are preferred to old-style class declarations.
If we create subclasses, then we should use class notation.
Don’t Manipulate prototypes Directly
With the class syntax, we shouldn’t manipulate prototypes directly.
This way, we can add methods and properties in a clearer fashion.
For instance, instead of writing:
function Foo() {
//...
}
Foo.prototype.bar = function() {
//...
}
We write:
class Foo {
bar() {
//...
}
}
Getters and Setters
We may use getters and setters. We can define them with the get
and set
keywords respectively.
However, we should never change variables in a getter.
For instance, we shouldn’t write:
class Foo {
get next() { return this.newId++; }
}
Overriding toString
We can override toString
as long as it doesn’t commit any side effects.
For instance, we just use them to return some values in string form:
class Foo {
toString() {
return `bar: ${this.bar}`;
}
}
Functions
There are many things to think of when we’re declaring and using functions.
Top-Level Functions
We should use the export
keyword to export top-level functions.
For instance, we can write:
export const foo = () => {
//..
};
Nested Functions
Functions can have functions inside it.
If we have nested functions, we should assign it to a const
variable.
For instance, we can write:
const bar = () => {
const baz = () => {
//...
}
//...
}
Arrow Functions
Arrow functions are great since it provides a concise syntax and it doesn’t bind to this
.
We should always use arrow functions over function
functions.
This way, we don’t have to use bind
or const self = this;
to set the value of this
inside a function.
The left-hand side of the arrow may contain 0 or more parameters.
Parentheses around parameters are optional if there’s only a single non-destructured parameter.
For instance, we can write:
const add = (a, b) => a + Number(b);
Generators
Generator functions let us return generators so that we can retrieve items sequentially.
For instance, we can write:
function* gen() {
yield 2;
yield 3;
}
We can also call another generator inside a generator function with the yield*
keyword.
For instance, we can write:
function* gen() {
yield* gen1();
}
where gen1
is a generator function.
Conclusion
We should use the class syntax as much as possible.
If we want to export top-level functions in a module with the export
keyword.
Also, we should assign nested functions to const
.
We can also override toString
in our classes.