for ES6 beginners
If you use the recommend class
syntax, you will get automatic errors without having to manually check this
or new.target
-
class Range {
constructor(from, to) {
Object.assign(this, {from, to})
}
}
// with `new`
const p = new Range(1,2)
console.log(p)
// without `new`
const q = Range(1,2)
console.log(q)
{ from: 1, to: 2 }
TypeError: Cannot call a class constructor without |new|
Instead of overriding Range.prototype
with a handwritten object, you can define so-called instance methods directly in the class
-
class Range {
constructor(from, to) {
Object.assign(this, {from, to})
}
includes(x) {
return x >= this.from && x <= this.to
}
toString() {
return `[${this.from}, ${this.to}]`
}
}
const p = new Range(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false
ES2020 and beyond
I will mention that there is growing support for modular design over large classes. If Range
were to include 20 methods but the caller only uses 1 or 2 of them, 18 methods are bundled into the final app but remain unused. It would take an extremely sophisticated compiler to remove this dead code, a process called tree shaking. Below we see range
written as a module instead of a class. Note only public methods are made accessible using export
, allowing the module author to restrict access to private methods wherever she/he wishes -
// range.js
const tag = Symbol()
const range = (from, to) => ({ tag, from, to })
const isRange = (t) => t.tag === tag
const includes = (t, x) => x >= t.from && x <= t.to
const toString = (t) => `[${t.from}, ${t.to}]`
export { range, isRange, includes, toString }
In the main
module, the caller only import
s what is needed, allowing a compiler to effectively prune any unused portions of the imported modules -
// main.js
import { range, includes, toString } from "./range.js"
const p = range(3,6)
console.log(`p: ${toString(p)}`)
console.log(`p includes 4: ${includes(p,4)}`)
console.log(`p includes 9: ${includes(p,9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false
For more info on import
, see Dynamically Importing ES modules by Alex Rauschmayer.
For more insight on the growing ES module initiative, see projects like v9 of Google Firebase's new API.
- Version 8. This is the JavaScript interface that Firebase has maintained for several years and is familiar to Web developers with existing Firebase apps. Because Firebase will remove support for this version after one major release cycle, new apps should instead adopt version 9.
- Modular version 9. This SDK introduces a modular approach that provides reduced SDK size and greater efficiency with modern JavaScript build tools such as webpack or Rollup.
Version 9 integrates well with build tools that strip out code that isn't being used in your app, a process known as "tree-shaking." Apps built with this SDK benefit from greatly reduced size footprints. Version 8, though available as a module, does not have a strictly modular structure and does not provide the same degree of size reduction.
...
have your cake and eat it too
Using a simple technique, we can write modular-based code and still offer an object-oriented interface, if desired. Below we write a Range
class as a simple wrapper around the module functions above -
// range.js
// ...
class Range {
constructor(t) { this.t = t }
isRange() { return isRange(this.t) }
includes(x) { return includes(this.t, x) }
toString() { return toString(this.t) }
static new(a,b) { return new Range(range(a,b)) }
}
export { Range, ... }
In your main module, you can import the Range
class to access all of the module's features through an object-oriented interface -
// main.js
import { Range } from "./range"
const p = Range.new(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)
Now your module can be used in functional style or object-oriented style. Note, when used this way the object-oriented interface comes at the cost of not being able to tree-shake this portion of the code. However, programs that wish take advantage of dead code elimination can use the functional style interface instead.
Verify in this runnable demo below -
// range.js
const tag = Symbol()
const range = (from, to) => ({ tag, from, to })
const isRange = (t) => t.tag === tag
const includes = (t, x) => x >= t.from && x <= t.to
const toString = (t) => `[${t.from}, ${t.to}]`
class Range {
constructor(t) { this.t = t }
isRange() { return isRange(this.t) }
includes(x) { return includes(this.t, x) }
toString() { return toString(this.t) }
static new(a,b) { return new Range(range(a,b)) }
}
// main.js
const p = Range.new(3,6)
console.log(`p: ${p}`)
console.log(`p includes 4: ${p.includes(4)}`)
console.log(`p includes 9: ${p.includes(9)}`)
p: [3, 6]
p includes 4: true
p includes 9: false
I have written about this technique in other answers -