DEV Community

John Au-Yeung
John Au-Yeung

Posted on • Updated on

Introducing JavaScript Arrow Functions

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

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.

Top comments (3)

Collapse
 
ahepa profile image
iosif hamlatzis

Coming from cpp world, I understood the syntax, but don't get the advantage of arrow functions

Collapse
 
aumayeung profile image
John Au-Yeung

The benefit of arrow functions is that you don't have to worry about what this is and accidentally using this in the wrong place.

Collapse
 
flayer2021 profile image
ANTONIO CARLOS

Legal
As arrows são top de mais ...