Arrow functions are a new type of functions introduced with ES2015 that provides an alternative compact syntax to functions declares with the function
keyword. They don’t have their own bindings to this
, argument
, super
or new.target
keywords. This means that they aren’t suited for methods since they don’t bind to this
, argument
, or super
. This also means that they can’t be used as constructors.
Declare Arrow Functions
To declare arrow functions, we can write the following if the function is one line long:
() => 2
The function above returns the number 2. Notice that for functions that are only one line long, we don’t need the return
keyword to return anything. Whatever is at the end is returned. Also notice that if it’s has no parameters than we need to put empty parentheses for the function signature. If there’s one parameter, then we can write something like:
x => x*2
The function above takes in a parameter x
and multiplies x
by 2 and then return it. Notice that for arrow functions that only has one parameter, we don’t need to wrap it inside parentheses. If we have more than 1 parameter, then we need parentheses around the parameters, like in the following code:
(x,y) => x*y
In the function above, we take x
and y
as parameters and return x*y
. If we want to return an object literal in a one-line arrow function, we have to put parentheses around the object literal that we want to return. For example, we write something like:
() => ({ a: 1, b: 2 });
to return the object { a: 1, b: 2 }
.
If we need to define a function that spans multiple lines, then we need to wrap curly brackets around the function’s code as we have below:
(x, y) => {
return x+y;
}
So the general syntax for arrow functions are one of the following:
(p1, p2, …, pN) => { statements } (p1, p2, …, pN) => expression
(p) => { statements }p => { statements }
() => { statements }
Default Parameters
Arrow functions’ parameters can have default values set for them so that we can kip them when are calling functions and still get a value set to them. Default parameters is easy with JavaScript. To set them in our code, we write the following:
const sum = (a,b=1) => a+b
In the code above, we set the default parameter value of b
to 1, so if the second argument isn’t passed in when we call the sum
function, then b
is automatically set to 1. So if we call sum
as follows:
sum(1)
we get 2 returned.
As we can see, now we don’t have to worry about having optional arguments being undefined
, which would be the alternative result if no default value if set for parameters. This eliminates many sources of errors that occurs when making parameters optional with JavaScript.
The alternative way without using default parameters would be to check if each parameter is undefined
. We write the following to do this:
const sum = (a,b,c)=>{
if (typeof b === 'undefined'){
b = 1;
}if (typeof c === 'undefined'){
c = 1;
}
return a+b+c;
}
If we call the following sum
function without some parameters, then we get:
const sum = (a,b = 1,c = 1)=> a+b+c;
sum(1) // returns 3
sum(1, 2) // returns 4
As we can see, we handled missing parameters gracefully with default parameters.
For default parameters, passing in undefined
is the same as skipping the parameters. For example, if we call the following function and pass in undefined
, then we get:
const sum = (a,b = 1,c = 1)=> a+b+c;
sum(1,undefined) // returns 3
sum(1,undefined,undefined) // returns 3
sum(1, 2,undefined) // returns 4
Note that undefined
is the same as skipping parameters. This isn’t true for other falsy values that are passed into a function. So if we pass in 0, null
, false
, or empty string, then they get passed in, and the default parameter values will be overwritten. For example, if we have the code below:
const test = (num=1) => typeof num;
test(undefined);
test(null);
test('');
test(0);
test(false);
We get number
for test(undefined)
, object
for test(null)
, string
for test(string)
, number
for test(0)
, and boolean
for test(false)
. This means that anything other than undefined
is passed in, but note that if we pass in falsy values and then run arithmetic operations on them, then falsy values get converted to 0.
const sum = (a,b = 1,c = 1)=> a+b+c;
sum(1,null)
sum(1,null,false)
sum(1, 2,'')
So sum(1, null)
returns 2 since b
is converted to 0 and c
has the default value of 1. sum(1)
returns 1 since b
and c
are converted to 0. sum(1, 2,’’)
is 3 since a
is 1, b
is passed in so that it becomes 2 instead of getting the default value of 1, and c
is an empty string which is converted to 0.
Default arguments are evaluated at call time so that they’re set each time they’re called if no argument is passed into the function parameter with default values. For example, if we have:
const append = (val, arr = [])=>{
arr.push(val);
return arr;
}
append(1);
append(2);
append(1)
returns [1]
and append(2)
returns [2]
since we didn’t pass in an array to each function call, so arr
is set to an empty array each time it’s run.
It’s also important to know that we can pass in values returned by functions in the default parameter, so if we a function that returns something, we can call it in the default parameter and assign the returned value to the parameter. For example, we can write:
const fn = () => 2
const sum(a, b = fn()) => a+b;
Then if we call sum(1)
, we get 3 since fn
function returns 2. This is very handy if we want to manipulate and combine values beforehand before assigning it as a parameter.
Another great feature of the default parameters is that the parameters left of the given parameter is available for the parameter for assignment as default values, so we can have a function like the function below:
const saySomething = (name, somethingToSay, message = \`Hi ${name}. ${somethingToSay}`) => ({
name,
somethingToSay,
message
});
Notice that we assigned an expression to the message
parameter in the saySomething
function. This is great for manipulating data and then assigning as we did before by assigning the function. We can also see that we can have default parameters that depend on parameters that are to the left of it. This means that default parameters to not have to be static.
So we call it with the first 2 parameters filled in, like saySomething(‘Jane’, ‘How are you doing?’)
. We get:
{name: "Jane", somethingToSay: "How are you doing?", message: "Hi Jane. How are you doing?"}
The message
is returned with the template string that we defined evaluated.
We cannot call functions nested inside a function to get the returned value as the default parameter value. For example, if we write:
const fn = (a = innerFn()) => {
const innerFn = () => { return 'abc'; }
}
This will result in a ReferenceError
being thrown because the inner function isn’t defined yet when the default parameter is defined.
We can also have default parameter values that are set to the left of the required parameters. Arguments are still passed into parameters from the left to the right, so is we have:
const sum = (a=1,b) => a+b
If we have sum(1)
we have NaN
return 1 is added to undefined
since we didn’t pas in anything for the second argument, b
is undefined
. However, if we write sum(1,2)
then 3 is returned since we have a
set to 1 and b
set to 2.
Finally, we can use destructuring assignment to set default parameter values. For example, we can write:
const sum = ([a,b] = [1,2], {c:c} = {c:3}) => a+b+c;
Then we call sum
without arguments we get 6 since a
is set to 1, b
is set to 2, and c
is set to 3 by the destructuring assignment feature that JavaScript provides.
This Object
If this
is referenced in a regular function declared with the function
keyword, the this
object isn’t set inside an arrow function to the function that has the this
inside. If an arrow function is inside a constructor, then this
is set to the new object. If it’s not inside any object, then this
inside the arrow function is undefined
in strict mode. this
will be set to the base object if the arrow function is inside an object. However, we always get the window
object if we reference this
in an arrow function. For example, if we log this
inside an arrow function that’s not inside a constructor or class like in the following code:
const fn = () => thisconsole.log(fn);
We get the window
object logged when console.log
is run. Likewise, if we ran:
let obj = {}
obj.f = () => {
return this;
};
console.log(obj.f());
We get the same thing as we got before. This is in contrast to the tradition functions declared with the function
keyword. If we replace the functions above with traditional functions in the code above, like in the following code:
const fn = function() {
return this
};
console.log(fn);
We get the function fn
that we declared logged for fn
.
However, if the arrow function is inside a constructor, we get the object that it’s the property of as the value of this
. For example, if we have:
const obj = function() {
this.fn = () => this;
}
console.log(new obj().fn())
Then this
will be the current object the this.fn
function is in, which is obj
, so obj
will be logged by console.log(new obj().fn())
. Also, if we have the code below:
let obj = {}
obj.f = function() {
return this;
};
console.log(obj.f());
We get the obj
object logged when it’s run.
When arrow functions are called with call
and apply
, the argument for the this
object will be ignored. This means that they can’t be used to change the this
object. For example, if we have:
function add(a) {
return this.base + a;
}
console.log(add.call({
base: 1
}, 2));
We get 3 logged. This means that we can pass in an object that we want for this
and get its value with traditional functions. This wouldn’t work with arrow functions. If we have:
const add = (a) => {
return this.base + a;
}
console.log(add.call({
base: 1
}, 2));
We get NaN
because this
is window
, which doesn’t have the base
property. The first argument for call
is ignored. Likewise, if we try to set the value of this
inside the function when we use apply
, we can do that with traditional functions:
function add(a) {
return this.base + a;
}
console.log(add.apply({
base: 1
}, [1]));
We get 2 logged since the first argument for apply
is the this
object for the function inside. This means that we can change this.base
to 1 by passing in our own object for the first argument of apply
. When we try the same thing with arrow functions like in the following code:
const add = (a) => {
return this.base + a;
}
console.log(add.apply({
base: 1
}, [1]));
We get NaN
because this.base
is undefined
. Again this
is set to the window
object since we have an arrow function, so this
cannot be changed with the apply
function.
The arguments
object isn’t bound to an arrow function, so we can’t use the arguments
object to get the arguments passed into a function call of an arrow function. If we have a traditional, we can get the arguments passed in with the arguments
object like the following code:
function add(){
return [...arguments].reduce((a,b) => +a + +b, [])
}
console.log(add(1,2,3));
We get 6 logged in the last line. This is because the arguments
object had all the arguments of the object in string form. So arguments
would have '1'
, '2'
, and '3'
listed. However, if we try to do that we an arrow function, we can’t get the sum of the arguments with the arguments
object. For example, if we have:
const add = () => [...arguments].reduce((a,b) => +a + +b, [])
console.log(add(1,2,3));
We get NaN
because we didn’t get the arguments of the function call in the arguments
object.
Arrow functions can’t be used as constructors, therefore we can’t instantiate it with thenew
keyword. For example, if we write the following and run it:
const Obj = () => {};
let obj = new Obj();
We would get a TypeError
.
Also, arrow functions don’t have a prototype
property, so when we log something like:
const Obj = () => {};
console.log(Obj.prototype);
We get undefined
. This means we can‘t inherit from an arrow function. The yield
keyword can’t be used in an arrow’s functions’s body. Therefore, we can’t use arrow functions as generator functions.
Arrow functions are useful for times when we need to write a function more concisely than writing traditional functions and we don’t need to change the content of the this
object inside the function. Also, we can’t use them for generator functions or constructor since they don’t have their own this
object. They also don’t have the prototype
property so they can’t be inherited from. Also, the arguments of an arrow function call cannot be retrieved from the arguments
object, so we just get them explicitly from the parameters.