Javascript 101
2.2 Arrow Functions
2.2 Arrow Functions
Another Function Declaration Syntax
In ES6, we have another function declaration syntax which is much shorter known
as the arrow function. The syntax is a little similar to lambda
expressions/functions present in other languages such as C# and Python. If there
is only an expression, the expression will be assumed to be the return value so
the return
keyword is optional. You will have to use the return
keyword if
there are multiple statements.
const add = function (n1, n2) { return n1 + n2;};
//arrow function (equivalent)//return is optional (assumed)const add = (n1, n2) => n1 + n2;
Arrow function can also be used to do side effect instead of returning values.
Similar to the function
syntax, we use { }
to enclose the statements. They
are often used for callback functions.
var array = [1, 2, 3, 4, 5];
array.forEach(function (i) { console.log(i);});
//equivalent://arrow function does not have to return something//if there is only 1 parameter, ( ) is optionalarray.forEach((i) => { console.log(i);});
If there is only 1 parameter, the enclosing ( )
for the parameter is optional.
However, if there is no parameter, we have to put ( )
so that there is no
syntax error.
const printMessage = (msg) => { console.log(msg);};
printMessage("blar"); //blar
//if no parameters, have to put ()const sayHello = () => { console.log("hello");};
sayHello(); //hello
Note that when there is only a single statement and we are not using the return
statement (like the above add
example), we must explicitly enclose with ( )
so that the JS engine will not interpret it as normal statements within the
{ }
block.
//invalidconst person = () => {name: 'John', age: 20};
//have to enclose with ()const person = () => ({name: 'John', age: 20});
console.log(person());//{name: "John", age: 20}
It might seem that arrow function is a perfect replacement for the function
syntax, and is preferred since it is much shorter. However, there is a
fundamental difference between the 2 constructs when dealing with objects and
the this
keyword.
Calling Context and Lexical Context
Before we discussed the key difference between arrow functions and functions
syntax, let us explore the notion of this
and context by which a function
is called.
function Circle(radius) { this.radius = radius;
this.getArea = function () { return Math.PI * this.radius * this.radius; };}
let c1 = new Circle(3);console.log(c1.getArea()); // PI * 3 * 3 = 28.274...
Suppose now we change the above example slightly, instead of calling getArea()
directly, we get the getArea()
function reference from the c1
object and
after some time, we call the function.
function Circle(radius) { this.radius = radius;
this.getArea = function () { return Math.PI * this.radius * this.radius; };}
let c1 = new Circle(3);
//get the function of the area function in the c1 objectconst areaFunction = c1.getArea;
//call itconsole.log(areaFunction()); //NaN
We will realize that the results from the getArea()
function invocation now
returns a NaN
instead of the usual 28.274. Why is the result NaN
? Is the
function even executed?
Let us add a console.log(this);
line in getArea()
to print out the this
reference. It turns out that the context by which getArea()
is called is
different compared to the previous case. Thus, the this
reference in
getArea()
in this case is not referring to the c1
object. Instead, it is
referring to the window
object.
function Circle(radius) { this.radius = radius;
this.getArea = function () { //print out the "this" reference console.log(this); return Math.PI * this.radius * this.radius; };}
let c1 = new Circle(3);
//get the function of the area function in the c1 objectconst areaFunction = c1.getArea;
//call itconsole.log(areaFunction());//Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}//NaN
Another Example - Clock
Consider the following example which defines a Clock object that will increment
the time value every second (using setInterval()
). We would expect it to print
5
after 5 secs.
function Clock() { this.time = 0;
//setInterval will execute the callback function every 1000ms (1s) setInterval(function () { this.time++; }, 1000);}
var clock1 = new Clock();//wait 5 secsconsole.log(clock1.time);
If we execute the above codes, we will realize the value of clock1.time
is
always 0. To see why is this happening, let us print out the this
reference
each time the callback function in setInterval()
is invoked.
function Clock1() { this.time = 0;
//setInterval will execute the callback function every 1000ms (1s) setInterval(function () { console.log(this); }, 1000);}
var clock1 = new Clock1();//a series of Window object is printed every sec//Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
Again, we see that the context that the callback function for setInterval()
is
getting is the window
object. This example demonstrates that functions in
objects might not work the way how we might expect, and sometimes
counter-intuitive to the programming logic.
Solution1: Define a variable pointing to this
and use it inside the function instead of using this
directly
To fix this, we often define a separate variable capturing the this
reference.
function Clock() { this.time = 0;
//define a variable that is capturing the "this" reference let self = this;
//setInterval will execute the callback function every 1000ms (1s) setInterval(function () { self.time++; }, 1000);}
var clock1 = new Clock();//wait 5 secsconsole.log(clock1.time);
Solution2: Use the bind() method
Each function
comes with a bind()
prototype method that allows us to bind
the this
context received by the function.
function Circle(radius) { this.radius = radius;
this.getArea = function () { //print out the "this" reference console.log(this); return Math.PI * this.radius * this.radius; };}
let c1 = new Circle(3);
//get the function of the area function in the c1 objectconst areaFunction = c1.getArea.bind(c1);
//call itconsole.log(areaFunction());//Circle {radius: 3, getArea: ƒ}//28.274...
function Clock() { this.time = 0;
//define the callback function and bind its "this" variable const cb = function () { this.time++; }.bind(this);
//setInterval will execute the callback function every 1000ms (1s) setInterval(cb, 1000);}
var clock1 = new Clock();//wait 5 secsconsole.log(clock1.time);
this
in function
is derived from the calling context
Without using bind()
to explicitly bind the context, the this
reference in
function
is derived from the context by which the function is called
rather than where they were defined.
The same problem happens when we are trying to do a Asynchronous Javascript
and XML (AJAX) call in an object, and then updating the fields of the object
based on the results of the AJAX call - this
will not be referring to the
current object! We have to use the same trick as before. This is quite a common
use case and developers have been using this approach to make the program work
as expected. The Fetch API (fetch()
and Promises
(.then()
keyword) will be discussed in later sections.
function Person() { this.name = null; this.phone = null;
let self = this;
//call an API using the Fetch API //and update the name and gender field in this object this.getRandomPerson = function () { fetch("https://jsonplaceholder.typicode.com/users/1") .then(function (response) { return response.json(); }) .then(function (person) { self.name = person.name; self.phone = person.phone; }); };}
var person1 = new Person();person1.getRandomPerson();
//wait a while until the data has been fetchedconsole.log(person1);
Lexical Context
It turns out that the more elegant approach is to use arrow functions. So the above program would look like this (just like how we would hope it to work):
function Person() { this.name = null; this.phone = null;
//call an API and update the name and gender field in this object this.getRandomPerson = function () { fetch("https://jsonplaceholder.typicode.com/users/1") .then((response) => response.json()) .then((person) => { this.name = person.name; this.phone = person.phone; }); };}
var person1 = new Person();person1.getRandomPerson();
//wait a while until the data has been fetchedconsole.log(person1);
Basically, arrow functions do not have its own this
, instead the this
reference in an arrow function is bound to its enclosing environment (i.e.
lexical context). This means that the reference to this
is based on how it
is defined in the code rather than based on how it is called during runtime.