DEV Community

Sergii Shymko
Sergii Shymko

Posted on • Updated on

Arrow Function vs Function

In JavaScript, arrow functions provide a concise syntax for anonymous function expressions stripped off of their OOP baggage. They are a syntactic sugar on a subset of the function capabilities. Both can be used as closures capturing variables of the outer scope.

Arrow functions are part of the ECMAScript 2015 standard also known as ES6. We will unpack variations of the arrow function ES6 syntax to their analogous function implementation and discuss the differences.

The article assumes familiarity with the traditional functions and builds on the prior knowledge by drawing parallels between the two language mechanisms.

Syntax

The "fat arrow" syntax => is dedicated to arrow functions, hence the name.

Arrow function declaration:

(arg1, arg2, ..., argN) => expression
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

(function (arg1, arg2, ..., argN) {
  return expression;
}).bind(this)
Enter fullscreen mode Exit fullscreen mode

There's a lot going on here: omitted keywords, the implicit return statement, this context binding. Each aspect is discussed separately below.

Semantics

Return Expression

Unlike ordinary functions (anonymous or otherwise), arrow functions implicitly return an evaluated expression without having to use the return statement.

Arrow function:

(arg1, arg2, ..., argN) => expression
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

function (arg1, arg2, ..., argN) {
  return expression;
}
Enter fullscreen mode Exit fullscreen mode

Once you get used to the syntax, you'll appreciate how much shorter the code becomes and would never want to go back.

Block Statement

The short return expression syntax cannot represent sequence of statements. That's where the familiar block statement {} comes in. Within the curly braces you'd have to explicitly return result of the function.

Arrow function:

(arg1, arg2, ..., argN) => {
  let result = doSomething();
  doDependentThing(result);
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

function (arg1, arg2, ..., argN) {
  let result = doSomething();
  doDependentThing(result);
  return result;
}
Enter fullscreen mode Exit fullscreen mode

The functions look more alike now, don't they?

Object Expression

Functions often return newly constructed objects. There's a catch: the object declaration notation {} is indistinguishable from the block statement syntax. The solution is to surround the inline object with () to make it an expression.

Arrow function:

(arg1, arg2, ..., argN) => ({
  prop1: value1,
  prop2: value2,
  ...,
  propN: valueN
})
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

function (arg1, arg2, ..., argN) {
  return {
    prop1: value1,
    prop2: value2,
    ...,
    propN: valueN
  };
}
Enter fullscreen mode Exit fullscreen mode

Single Argument

There's an extra syntactic sugar for a special case of an arrow function having only one argument. You can omit the parentheses () around the argument.

Arrow function:

arg => expression
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

function (arg) {
  return expression;
}
Enter fullscreen mode Exit fullscreen mode

No Arguments

An arrow function w/o arguments is just an edge case of empty parentheses. Unlike the single argument syntax, the parentheses are required here.

Arrow function:

() => expression
Enter fullscreen mode Exit fullscreen mode

Equivalent anonymous function:

function () {
  return expression;
}
Enter fullscreen mode Exit fullscreen mode

Context Binding

Let's talk about the elephant in the room – the this context. Arrow functions aside, this (pun intended) has always been a confusing topic in JavaScript.

Functions have access to a special variable this holding the context assigned in runtime. The problem is that the value varies depending on how the function is called which is error-prone and often undesirable.

With callbacks being the primary use case, in most cases you'd want access to this context defined at a declaration time, not at invocation. You'd find yourself sprinkling your code with the following closure boilerplate:

let self = this;
let callback = function () {
  self.doSomething();
};
Enter fullscreen mode Exit fullscreen mode

or the re-binding to avoid self in the callback:

let callback = function () {
  this.doSomething();
};
callback = callback.bind(this);
Enter fullscreen mode Exit fullscreen mode

In contrast, arrow functions provide no this context of their own and instead inherit the current "lexical" scope. They're naturally suited for inline callbacks.

Equivalent arrow function:

let callback = () => void this.doSomething();
Enter fullscreen mode Exit fullscreen mode

The void operator discards the result returned by this.doSomething(), if any. In practice, passing the result though is often okay and void can be omitted. The block statement {} is another (perhaps better) way to ignore the result.

Class Methods

Arrow functions come in handy in classes due to the nature of this context. Ordinary methods are prone to losing class context when called from outside of class methods. Arrow methods are immune to this issue.

The arrow method syntax is nothing but a class property declaration with an arrow function assigned in place of the value. Note the class properties are introduced in the ECMAScript 2017 specification.

Arrow method (arrow function property):

class Example {
  constructor(arg) {
    this.arg = arg;
  }

  callback = () => {
    console.log(this.arg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Equivalent ES6 class method:

class Example {
  constructor(arg) {
    this.arg = arg;
    this.callback = this.callback.bind(this);
  }

  callback() {
    console.log(this.arg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Examples

Loop Refactoring

Single argument is quite common in array method callbacks, such as map() and its cousins, that iterate over items.

Loop over array of items:

let ids = [];
for (let i = 0; i < items.length; i++) {
  ids.push(items[i].id);
}
return ids;
Enter fullscreen mode Exit fullscreen mode

Equivalent traditional function implementation:

let ids = items.map(function (item) {
  return item.id;
});
Enter fullscreen mode Exit fullscreen mode

Equivalent arrow function implementation:

let ids = items.map(item => item.id);
Enter fullscreen mode Exit fullscreen mode

This example vividly demonstrates the level of code compression provided by arrow functions without sacrificing readability and even improving it.


Enjoy the utility of arrow functions in your modern JavaScript code!

Top comments (9)

Collapse
 
terpinmd profile image
terpinmd

Hmm. I think there are some issues with this write-up. Not sure why you are calling function() closures. Both "function" and fat arrow are functions, and they both can be closures, with the difference being their lexical scope.

A closure is just a function that has access to variables in its scope. Its really more of a pattern/feature.

Also I don't know that "function" is more or less OOP than fat arrow.

I think you should consider editing this article, at minimum the terminology is not correct for what you are calling a closure

Collapse
 
sshymko profile image
Sergii Shymko • Edited

Thanks for a very detailed constructive feedback! Much appreciated!

Indeed, coming from PHP, I've used the term "closure" synonymously to "anonymous function" that can optionally reference the outer scope (becoming a closure). Agreed, it's unacceptable in JavaScript and strictly speaking incorrect. Updated the article accordingly.

The OOP remark referred to inability to use arrow functions as class constructors. See the code example in the comment below. Will try to rephrase or expand on this topic to avoid confusion.

P.S.

the best way to get the right answer on the internet is not to ask a question; it's to post the wrong answer.

Cunningham's Law

Thanks!

Collapse
 
piyush1104 profile image
Piyush Bansal

Closures can do everything that arrow functions can do, but not vice versa.

Can someone tell me what are the things that are not doable by arrow functions?

Collapse
 
sshymko profile image
Sergii Shymko

Arrow functions cannot be used as class constructors, for instance:

const Class1 = function () {};
const Class2 = () => {};

let obj1 = new Class1();  // Ok
let obj2 = new Class2();  // TypeError: Class2 is not a constructor
Enter fullscreen mode Exit fullscreen mode
Collapse
 
piyush1104 profile image
Piyush Bansal

Cool, this is something new I didn't know.

Collapse
 
sshymko profile image
Sergii Shymko • Edited

Arrow functions cannot be called with a dynamic this scope useful to reuse the same callback on different elements.

For example:

let button1 = document.getElementById('button1');
let button2 = document.getElementById('button2');

let callback = function() {
  console.log(`Button ${this.id} clicked.`);
};

button1.onclick = callback;
button2.onclick = callback;
Enter fullscreen mode Exit fullscreen mode

It will log button1 and button2 respectively. Such behavior cannot be reproduced by a single arrow function. You'd have to declare two arrow functions each enclosing the context of the respective element.

Collapse
 
piyush1104 profile image
Piyush Bansal

Can't we just get this info from 'event.target'?

Thread Thread
 
sshymko profile image
Sergii Shymko

Good point. That's true for DOM events, but not in general case of arbitrary callback.

Collapse
 
sshymko profile image
Sergii Shymko

Removed that sentence as the article does not expand on this topic. Thanks!