Many developers were frustrated about JavaScript, especially before ECMAScript 2015 standard. And they had the right too: the language is known for its weakness and initial design drawbacks.
How did it happen?
At the beginning of the web era, no one did know how exactly the web will look in 10 - 15 years. And especially that a small Java's brother JavaScript will become the dominant language in this area, especially on the client side.
It is complicated to predict the divergent evolution of the Internet technology.
It's like fixing a giant rocket on the full speed, moving to Earth's orbit.
When things started to clear up, it wasn't so simple to head into the right direction.
A lot of codebase was written already: the rocket was launched. The backward compatibility and the growing list of hacks slow down the web development. Because really it should be extensible, scalable, easy and fun.
What a developer can do? Knockout JavaScript hacks!
Now it's the right time, especially that ECMAScript 2015 and beyond standards are sufficient to start coding the correct way.
So, let's continue with another series of improvements!
1. String literal as template
Embedding variables into a string literal? Don't forget to pair + and '.
Embedding variables into a string literal was always a tedious task.
It's ok for one variable 'str 1 ' + var1
. It becomes harder when the construction grows 'str 1 ' + var1 + ' str 2' + (var2 + var3) + ' str 3'
. You have to spend time pairing +
and '
characters, and the whole expression is difficult to understand.
Let's look at the neighbor. In Python you can format strings easily, because the string literal and variables are separated:
animal = 'monkey'countFruits = 4fruit = 'bananas''A %s has %d %s' % (animal, countFruits, fruit)# => 'A monkey has 4 bananas'
Compare the Python style with JavaScript's ES5 style, where variables and literals are mixed. This mix creates a construction that is complicated to follow:
var animal = 'monkey';var countFruits = 4;var fruit = 'bananas';'A ' + animal + ' has ' + countFruits + ' ' + fruit;// => 'A monkey has 4 bananas'
Obviously the amount of concatenation operators +
and single quotes '
does not make the entire string readable.
ECMAScript 2015 presents a new type of string literal called template literal. Fortunately it allows to embed JavaScript expressions into place holders ${expression}
and create strings as easy as a pie.
Let's see how nicely literals are looking now:
var animal = 'monkey';var countFruits = 4;var fruit = 'bananas';`A ${animal} has ${countFruits} ${fruit}`// => 'A monkey has 4 bananas'
No more redundant concatenation operators and single quotes. The template literal makes the string easy to follow.
Additionally template literals allow to write strings in multiple lines:
var templateLiteral = `Line 1Line 2Line 3`;
And compose so called tagged template literals, by placing a function encodeUrlParams
before the template string:
function encodeUrlParams(literals, ...substitutions) { var str = ''; // run the loop only for the substitution count for (var i = 0; i < substitutions.length; i++) { str += literals[i]; str += encodeURIComponent(substitutions[i]); } // add the last literal str += literals[literals.length - 1]; return str;}var redirectUrl = 'http://site2.com';encodeUrlParams `http://example.com?redirect=${redirectUrl}`;// => 'http://example.com?redirect=http%3A%2F%2Fsite2.com'
encodeUrlParams()
is a function that encodes the strings to be used safely as URL parameters.
Inside the function literals
parameter is an array that contains the literals parts. substitutions
contains the evaluation result of the expressions, which in our case are the params to be encoded.
Template literals availability:
Standard | ECMAScript 2015 |
Browsers/runtime support | Chrome 41+, Firefox 34+, Safari 9+, Edge 13+, Node 4+ |
Babel transform | ES2015 template literals transform |
2. Verify substring existence in a string
string.indexOf(substring) !== -1
? Could be better!
In ES5, searching for a substring in a string does not provide a straightforward solution. The most appropriate way is to use string.indexOf(substring, [fromIndex])
method. Then compare the returned index with the hardcoded value -1
, which indicates that the substring is not found:
var str = 'Hello World';str.indexOf('Hello') !== -1; // => truestr.indexOf('Hi') !== -1; // => false
str.indexOf('Hello')
returns 0
: the index of 'Hello'
substring. 0 !== -1
comparison evaluates to true
.
For a missing substring 'Hi'
, the code str.indexOf('Hi')
returns -1
. So -1 !== -1
comparison evaluates to false
.
Even the hack explanations are clumsy: something is not because something is not.
Finally ECMAScript 2015 provides a correct method to verify a substring existence: string.includes(substring, [fromIndex])
. The method returns a boolean that indicates if substring
is a substring of string
, optionally starting from an index fromIndex
.
The above example is now transformed to a clear solution:
var str = 'Hello World';str.includes('Hello'); // => truestr.includes('Hi'); // => false
str.includes('Hello')
evaluates to true
, because 'Hello'
is an existing substring.
And for a non-existing substring 'Hi'
the code str.includes('Hi')
returns false
.
No more alternatives. Refactor your project by changing from string.indexOf(substring) !== -1
to string.includes(substring)
and be happy as a clam.
.includes()
string method availability:
Standard | ECMAScript 2015 |
Browsers/runtime support | Chrome 41+, Firefox 40+, Safari 9+, Edge 13+, Node 4+ |
Polyfill | String.prototype.includes() polyfill |
3. Function call with dynamic list of arguments
array.push.call(array, elements)
? We can do better.
Many JavaScript functions and methods accept a dynamic number of arguments. For example console.log(text1, text2, ...)
, array methods .push()
, .concat()
, etc.
They work nicely when you enumerate manually the arguments:
var numbers = [1, 2, 3];numbers.push(4, 5);numbers // => [1, 2, 3, 4, 5]
In the expression numbers.push(4, 5)
two arguments are indicated: 4
and 5
. These elements are pushed into numbers
array.
Often an array holds the arguments. This case is not so simple. The option is to apply an indirect call using .apply()
method of the function object. Let's take a try:
var numbers = [1, 2, 3];var toPush = [4, 5];numbers.push.apply(numbers, toPush);numbers // => [1, 2, 3, 4, 5]
The indirect call to push some elements from an array looks clumsy: numbers.push.apply(numbers, toPush)
.
Calling an additional method .apply()
is the first problem. Short code is always better than lengthy one.
The second problem is a redundant indication of the context .apply(numbers, ...)
. The whole indirect call expression in the end contains 2 times numbers
object: numbers.push.apply(numbers, toPush)
.
To me the indirect call expression looks like a big lazy frog.
The spread operator ...
makes the function invocation from an array of arguments wonderful.
Simply put three dots ...
before the array when calling a function and its elements spread into invocation arguments. myFunction(...argsArray)
is equivalent to myFunction(argsArray[0], argsArray[1], ..., argsArray[n])
.
The above example can be improved significantly:
var numbers = [1, 2, 3];var toPush = [4, 5];numbers.push(...toPush);numbers // => [1, 2, 3, 4, 5]
When numbers.push(...toPush)
is executed, the spread operator ...
spreads toPush
array elements into arguments. Simple and beautiful as a sunrise.
A nice bonus: three dots ...
accepts not only arrays, but any iterable objects (strings, maps, sets, etc). Also you can use the operator to setup the arguments for a constructor call new Date(...[2017, 8, 1])
, which is not possible with indirect calls .apply()
.
Spread operator availability:
Standard | ECMAScript 2015 |
Browsers/runtime support | Chrome 46+, Firefox 27+, Safari 10+, Edge 13+, Node 6+ |
Babel transform | ES2015 spread transform |
4. Class hierarchy
I feel a bit nervous when I write
.prototype
.
Today's trending says to favor composition over inheritance. I would add: to favor only when necessary!
As an example, you can find the class inheritance useful when extending React components.
ES5 standard does not permit obviously to create a class hierarchy. It happens because JavaScript has a prototypal-based inheritance, and the class inheritance is emulated on top of it.
Let's define a superclass and extend it in a subclass:
function Figure(notation) { this.notation = notation;}Figure.prototype.getNotation = function() { return 'Figure notation: ' + this.notation;}function Square(notation, sideLength) { Figure.call(this, notation); this.sideLength = sideLength; }Square.prototype = Object.create(Figure.prototype);Square.prototype.constructor = Square;Square.prototype.getArea = function() { return Math.pow(this.sideLength, 2);};var mySquare = new Square('ABCD', 4);mySquare.getNotation(); // => 'Figure notation: ABCD'mySquare.getArea(); // => 16
Figure
is a superclass. Square
is a subclass of it, which inherits notiation
property and getNotiation()
method.
Square
class defines its own sideLength
property and getArea()
method.
As seen in the example, to mimic the class inheritance a lot of additional glue code is added:
- In the
Square
constructorFigure.call(this, notation)
makes a parent constructor call Square.prototype = Object.create(Figure.prototype)
creates a new prototype object forSquare
constructor.Square
instances inherit methods fromFigure.prototype
Square.prototype.constructor = Square
fixes the prototype to have the correct constructor property
If you get payed depending on how much lines of code you provide in 8 hours, this option may work. But normally this is too verbose.
ECMAScript 2015 class
makes the class inheritance usage much easier, providing a syntactic sugar over JavaScript's prototypal inheritance. Nice!
Let's improve the above geometrical class hierarchy using ES2015 class
syntax:
class Figure { constructor(notation) { this.notation = notation; } getNotation() { return 'Figure notation: ' + this.notation; }}class Square extends Figure { constructor(notation, sideLength) { super(notation); this.sideLength = sideLength; } getArea() { return Math.pow(this.sideLength, 2); }}var mySquare = new Square('ABCD', 4);mySquare.getNotation(); // => 'Figure notation: ABCD'mySquare.getArea(); // => 16
Indeed class
syntax makes the code more convenient:
- Use
class <ClassName>
to defined a class andclass <Class> extends <SuperClassName>
to define a subclass that extends a super class - A special method
constructor() {...}
is used as constructor to initialize the newly created instance with properties - In the constructor function
super()
calls the parent class constructor. Just be sure to executesuper()
before usingthis
If you're using React's React.createClass({...})
, it is the right time to migrate to ES2015 classes React.Component
.
class
syntax availability:
Standard | ECMAScript 2015 |
Browsers/runtime support | Chrome 49+, Firefox 45+, Safari 10+, Edge 13+, Node 6+ |
Babel transform | ES2015 classes transform |
5. Transform array-like object to array
Array-like objects... What could be more hacky?
I consider array-like objects a weird constructor. In the end, why not simply create arrays? Probably this is the answer for arguments
object. And because many DOM methods return an HTMLCollection
, which is a live array-like object that cannot be mutated (live means it's auto-updated when DOM changes).
Nevertheless exist situations when the array-like object needs to be transformed into a regular array, to take advantage of the array methods like .forEach()
, .map()
, etc.
Let's see how to transform an array-like object to an array using ES5 possibilities:
function argsToArray() { return Array.prototype.slice.call(arguments);}argsToArray(5, 6, 8) // => [5, 6, 8]
argsToArray()
returns an array that contains the arguments passed to function on invocation. To transform the array-like object arguments
into a real array, an indirect call to .slice()
method is applied: Array.prototype.slice.call(arguments)
.
Array.from(arrayLike, [mapFunction])
function transform arrayLike
parameter into an array and returns it.
Let's see Array.from()
in action:
function argsToArray() { return Array.from(arguments);}argsToArray(5, 6, 8) // => [5, 6, 8]
Array.from(arguments)
transforms arguments
array-like object into an array. This approach is shorter than the one presented above with an indirect call to .slice()
array method.
If you don't want to deal at all with arguments
object (and I recommend you to do that), define a rest parameter:
function argsToArray(...args) { return args;}argsToArray(5, 6, 8) // => [5, 6, 8]
Array.from()
availability:
Standard | ECMAScript 2015 |
Browsers/runtime support | Chrome 45+, Firefox 32+, Safari 10+, Edge 14+, Node 4+ |
Polyfill | Array.from() polyfill |
6. Don't stop me now
The article covered another list of hacks that should be migrated when possible. Thanks to ECMAScript 2015/2016 the migration adds big benefits.
Surely string.indexOf(substring) !== -1
should disappear from now on in favor of the correct string.includes(substring)
.
The template literal improves the code readability when variables should embed into a string. You could also insert newlines into template literals, which is a nice bonus.
To reduce the redundant code that mimics class inheritance over JavaScript's prototypal inheritance, it is recommended to use class
syntax. It is light and short.
However think twice before creating hierarchies of classes in JavaScript. Composition has a lot of flexibility, and consider it first as an option before diving into class inheritance mechanism.
Such a beautiful day to make your JavaScript code shine!