Callable entities in ECMAScript 6
In ECMAScript 5, one creates all callable entities via functions. ECMAScript 6 has more constructs for doing so. This blog post describes them.
The status quo: ECMAScript 5
In ECMAScript 5, functions do triple duty:
- As normal functions: you can directly call functions.
- As methods: you can assign a function to the property of an object and call it as a method, via that object.
- As constructors: you can invoke functions as constructors, via the new operator.
The three main problems with this approach are:
- It confuses people.
- You can use a function the wrong way, e.g. call a constructor or a method as a normal function.
- Functions used as normal functions shadow the this of surrounding constructors or methods [1]. That’s because this is always dynamic (provided by each function call), but should be lexical in this case, like normal variables that are resolved via surrounding scopes if they are not declared within a function.
Let’s first look at ECMAScript 6’s callable entities and then at how they help with these problems.
ECMAScript 6’s callable entities
The following subsections explain ECMAScript 6’s callable entities and what ECMAScript 5 constructs and patterns they correspond to. If you are confused about the difference between a function expression and a function declaration, consult [3].
Function expression → arrow function
In ECMAScript 5, a function used as a normal function inside a constructor or a method shadows this:
function GuiComponent() { // constructor
var that = this;
var domNode = ...;
domNode.addEventListener('click', function () {
console.log('CLICK');
that.handleClick(); // `this` is shadowed
});
}
ECMAScript 6 has arrow functions [2] that have a more compact syntax and don’t have their own this, their this is lexical:
function GuiComponent() { // constructor
var domNode = ...;
domNode.addEventListener('click', () => {
console.log('CLICK');
this.handleClick(); // `this` not shadowed
});
}
Function declaration → const + arrow function
In ECMAScript 5, you have function declarations for normal functions:
function foo(arg1, arg2) {
...
}
In ECMAScript 6, you’ll const-declare an arrow function:
const foo = (arg1, arg2) => {
...
};
The problem with function declarations is that they shadow this inside methods and constructors.
But they also have two advantages: First, a function object created by a function declaration always gets a meaningful name, which is useful for debugging. However, ECMAScript 6 engines will probably also assign names to arrow functions, at least in standard scenarios such as the one above.
Second, function declarations are hoisted (moved to the beginning of the current scope). That allows you to call them before they appear in the source code. Here, more discipline is required in ECMAScript 6 and source code will sometimes not look as nice (depending on your taste). However, one important case of calling methods and normal functions that appear later does not change: calling them from other methods and functions (after the callees have been evaluated!).
Ironically, not using function declarations may make things less confusing for newcomers, because they won’t need to understand the difference between function expressions and function declarations [3]. In ECMAScript 5, I’m often seeing code like this, using a function expression instead of a function declaration (even though the latter is considered best practice):
var foo = function (arg1, arg2) {
...
};
IIFE → block + let
ECMAScript 5 does not have block-scoped variables. In order to simulate blocks, you use the IIFE [1] pattern:
(function () { // open IIFE
var tmp = ...;
...
}()); // close IIFE
In ECMAScript 6, you can simply use a block and a let variable declaration:
{ // open block
let tmp = ...;
...
} // close block
Function in object literal → concise method syntax
In ECMAScript 5, you define a method inside an object literal by providing a property value via a function expression:
var obj = {
myMethod: function (arg1, arg2) {
...
}
};
In ECMAScript 6, you get more compact syntax for defining a method (internally, the result is the same):
let obj = {
myMethod(arg1, arg2) {
...
}
};
Constructor → class
In ECMAScript 5, you use constructors. Which becomes clumsy if you want to define sub-constructors:
// Super-constructor
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '('+this.x+', '+this.y+')';
};
// Sub-constructor
function ColorPoint(x, y, color) {
Point.call(this, x, y);
this.color = color;
}
ColorPoint.prototype = Object.create(Point.prototype);
ColorPoint.prototype.constructor = ColorPoint;
ColorPoint.prototype.toString = function () {
return this.color+' '+Point.prototype.toString.call(this);
};
In ECMAScript 6, you use classes [4] (which have the same method definition syntax as ECMAScript 6 object literals):
// Super-class
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '('+this.x+', '+this.y+')';
}
}
// Sub-class
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // same as super.constructor(x, y)
this.color = color;
}
toString() {
return this.color+' '+super();
}
}
New in ECMAScript 6: generator functions and generator methods
One construct is completely new in ECMAScript 6: generators [5]. You can create one via a generator function declaration:
function *generatorFunction(arg1, arg2) {
...
}
Or via a generator method definition (which can also be used in classes):
let obj = {
*generatorMethod() {
...
}
};
Generator functions are an unfortunate mix of the old and the new. Like function declarations, they have dynamic this. And there are no generator function expressions.
In my opinion, a better choice would be to replace generator function declarations with generator arrow functions. Or at least to additionally introduce the latter, with an asterisk somewhere. For example:
const generatorFunction = (arg1, arg2) =>* {
...
};
Alas, this idea has been explicitly rejected for ECMAScript 6, due to syntactic issues.
Avoiding function expressions
There are two cases where you may think you need old-school function expressions with dynamic this. This section shows you that you don’t.
Functions with this as an implicit parameter
Some libraries use this as an implicit parameter:
var $button = $('#myButton');
$button.on('click', function () {
this.classList.toggle('clicked');
});
If you are using such a library, you have no choice but to use functione expressions. If you are considering using this pattern for your own library then know that you don’t have to. You can always introduce an explicit parameter, instead:
var $button = $('#myButton');
$button.on('click', target => {
target.classList.toggle('clicked');
});
As an added benefit, the this of the surrounding scope remains accessible.
Adding methods to an object
To add a method to an existing object in ECMAScript 5, you use a function expression:
MyClass.prototype.foo = function (arg1, arg2) {
...
};
In ECMAScript 6, you can use Object.assign() and a method definition inside an object literal:
Object.assign(MyClass.prototype, {
foo(arg1, arg2) {
...
}
});
Conclusion
The large amount of callable entities in ECMAScript 6 can be a bit overwhelming at first. But they do help with the three problems of using functions for everything (#1 confusing, #2 can be used incorrectly, #3 dynamic this where you don’t want it):
- The clear separation of concerns makes things less confusing, especially for newcomers: classes replace constructors, blocks replace IIFEs, the keyword function does not appear when you define a method, etc.
- ECMAScript 6 prevents some incorrect uses of functions, but not many: you will get an exception if you invoke a class as a function. Calling extracted methods as functions remains a problem.
- Arrow functions eliminate the pitfall of inadvertently shadowing this.
Comments
Post a Comment