Tutorials

JavaScript ES6 Functions

Key Takeaways

  • JavaScript functions package reusable logic. Defining a function creates it; calling it executes its body.
  • Parameters name the inputs; arguments supply their values. Default parameters apply when an argument is omitted or is undefined.
  • Arrow functions offer concise syntax, but they also capture surrounding this and arguments rather than creating their own bindings.
  • Choose declarations for named operations, arrows for concise callbacks, and object method shorthand when a method needs its receiver as this.

JavaScript functions let you define an operation once and call it with different inputs. You can write them as declarations, traditional function expressions, or arrow functions—but arrows aren't simply shorter regular functions.

ES6, also called ECMAScript 2015, introduced arrow functions, default parameters, and rest parameters. This tutorial explains those features alongside the traditional function forms you'll still use. The ECMAScript 2015 specification defines these additions.

Why Use JavaScript Functions?

Suppose you want to add 50 to two numbers and print the results:

let a = 4;
let b = 3;

a += 50;
console.log(a); // 54

b += 50;
console.log(b); // 53

A function centralizes the repeated operation:

let a = 4;
let b = 3;

const addAndPrint = (x) => {
  x += 50;
  console.log(x);
};

addAndPrint(a); // 54
addAndPrint(b); // 53

console.log(a, b); // 4 3

Both snippets print the transformed numbers, but they don't make the same updates. In the function, x is a local parameter binding: assigning to it doesn't change a or b. If the caller needs an updated value, return that value and assign it in the caller.

The maintainability benefit is centralizing behavior. If the operation changes, you update one function rather than finding every duplicated copy.

The Anatomy of a Function

Bindings, Definitions, and Calls

A variable can hold a function value just as it can hold a number:

let x = 4;
const y = () => {};

y(); // Calls the function stored in y.

Here, y is a const binding holding an arrow function expression. The definition doesn't execute the body; the parentheses in y() invoke, or call, the function.

Parameters and Arguments

Parameters are the names in the function definition. Arguments are the values you pass when calling it:

const printSum = (a, b) => {
  console.log(a + b);
};

printSum(2, 5); // 7

In this example, a and b are parameters; 2 and 5 are arguments. During this call, the function adds those arguments and logs 7.

Default Parameters

A default parameter supplies a fallback when an argument is omitted or explicitly undefined:

const printSum = (a, b = 1) => {
  console.log(a + b);
};

printSum(1);            // 2
printSum(1, 2);         // 3
printSum(1, undefined); // 2
printSum(1, 0);         // 1

The default for b doesn't replace every falsy value. Values such as 0, false, an empty string, and null don't trigger it. See the default parameter reference for the precise rules.

Returning Values Versus Performing Actions

Use return when the caller needs a result:

const myFunction = (x) => {
  return x * 2;
};

const myValue = myFunction(2);
console.log(myValue); // 4

The call myFunction(2) produces 4, which becomes the value of myValue. A return statement also ends that function call's execution.

A function can instead perform a side effect: an action outside the calculation, such as logging a message.

const myFunction = () => {
  console.log("hello world");
};

const result = myFunction(); // Prints hello world.
console.log(result);         // undefined

The function must be called to print the message. When an ordinary synchronous function reaches the end of its body without returning a value, its result is undefined—not literally the absence of a value.

Rest Parameters for Multiple Inputs

A rest parameter collects the remaining arguments into an array:

const sum = (...numbers) => {
  let total = 0;

  for (const number of numbers) {
    total += number;
  }

  return total;
};

console.log(sum(2, 5, 3)); // 10
console.log(sum());        // 0

You can have only one rest parameter, and it must be last. Rest parameters work with arrows and ordinary functions; they're a clear way to express a variable number of inputs.

How to Define a Function in JavaScript

Function Declarations

A function declaration defines a named function directly:

function addDeclared(x, y) {
  return x + y;
}

console.log(addDeclared(1, 2)); // 3

Use declarations for named operations that deserve a clear place in your program. Their hoisting behavior also lets you call them before their declaration appears within the same scope.

Traditional Function Expressions

A function expression creates a function as part of an expression—for example, the initializer of a variable:

const addExpression = function (x, y) {
  return x + y;
};

console.log(addExpression(1, 2)); // 3

This is a function expression, not a function declaration or a special “ES5 mode.” It doesn't require var. Use this form when you want a function value with ordinary function behavior, including its own this and arguments bindings.

Arrow Functions and Implicit Returns

Arrow functions use the => syntax. An expression body implicitly returns its expression's value; a block body needs an explicit return to return a result:

const addArrow = (x, y) => x + y;

const addWithBlock = (x, y) => {
  return x + y;
};

console.log(addArrow(1, 2));     // 3
console.log(addWithBlock(1, 2)); // 3

To implicitly return an object literal, wrap it in parentheses so its braces aren't interpreted as a function body:

const makePoint = (x, y) => ({ x, y });

console.log(makePoint(2, 5)); // { x: 2, y: 5 }

Prefer a block body when several statements make the operation easier to follow. Concision helps only while the function remains readable.

Object Method Shorthand

For a function attached to an object, method shorthand makes the relationship explicit:

const calculator = {
  offset: 10,

  add(x, y) {
    return this.offset + x + y;
  }
};

console.log(calculator.add(1, 2)); // 13

In calculator.add(1, 2), calculator is the receiver—the object used for the method call—and this refers to it. Unlike a plain function expression, a shorthand method can't be used as a constructor. Explore the surrounding syntax in our JavaScript objects tutorial.

Function Forms at a Glance

FormExample syntaxthis behaviorAvailable before definition?Usable with new?
Declarationfunction add(a, b) { return a + b; }Determined by the callYes, within its scopeYes
Traditional expressionconst add = function (a, b) { return a + b; };Determined by the callNot before const initializationYes
Arrow expressionconst add = (a, b) => a + b;Captured from enclosing scopeNot before const initializationNo
Object method shorthandconst obj = { add(a, b) { return a + b; } };Receiver in obj.add()Not before object initializationNo

The constructor column describes the plain synchronous functions shown here, not async functions or generators. Constructor support comes from the function form, not from choosing const or let.

Scope and Hoisting: Declarations Versus Expressions

Scope is the region where a name is accessible. Hoisting means a declaration is processed before the statements in its scope execute; it doesn't mean JavaScript physically moves your source code.

console.log(addDeclared(1, 2)); // 3

function addDeclared(x, y) {
  return x + y;
}

// Calling addExpression here would throw ReferenceError.
const addExpression = function (x, y) {
  return x + y;
};

console.log(addExpression(1, 2)); // 3

A function declaration is initialized before execution reaches its position in the scope. A const or let binding is inaccessible until initialization: the earlier period is its temporal dead zone, and accessing it then throws ReferenceError. This applies whether the initializer contains an arrow or a traditional function expression.

Don't turn this into the blanket rule that expressions “aren't hoisted.” With var, the binding initially contains undefined, but the function value still isn't available before assignment. The function expression reference demonstrates that distinction.

Use const when you don't intend to reassign the binding, not as a general solution to naming conflicts. Manage names through clear naming and appropriate scopes. Our comparison of var, let, and const and MDN's temporal dead zone explanation cover the binding rules.

Arrow Functions Versus Regular Functions

Arrows Capture the Surrounding this

An ordinary function's this depends on how it is called. An arrow instead uses this from its enclosing lexical scope—the surrounding code where the arrow was created.

const user = {
  name: "Ada",

  makeGreeting() {
    return () => `Hello, ${this.name}`;
  }
};

const greet = user.makeGreeting();
console.log(greet()); // Hello, Ada

Calling user.makeGreeting() gives the method user as this. The returned arrow retains access to that surrounding this even when greet() is called without a receiver. This is a closure: a function keeping access to its surrounding environment.

Arrows Don't Have Their Own arguments

Ordinary functions provide an arguments object containing the arguments passed to that call. Arrows don't create their own arguments binding; a reference to arguments resolves through the surrounding scope instead. Use explicit parameters or rest parameters when an arrow needs its own inputs.

Arrows Aren't Constructors

Calling an arrow with new throws TypeError. Arrows also have no own .prototype property by default, unlike the plain traditional functions shown earlier. Assigning an arrow to a variable or an object property doesn't change those restrictions.

Functions as Values: Passing a Callback

A callback is a function you pass to another function for it to call. For example, map() builds a new array using a callback's returned values:

const double = (number) => number * 2;
const doubled = [1, 2, 3].map(double);

console.log(doubled); // [2, 4, 6]

Pass double, the function value—not double(), which would call it immediately. You can also write the arrow directly inside map():

const doubled = [1, 2, 3].map((number) => number * 2);

console.log(doubled); // [2, 4, 6]

The map() reference explains its callback contract. For more practice, continue with JavaScript arrays and array methods.

Common Mistakes

  • Defining without calling: storing a function in a variable doesn't execute its body.
  • Forgetting return after adding braces: changing an expression-bodied arrow to a block body requires an explicit return to preserve its result.
  • Using an arrow as a receiver-based method: lexical this doesn't automatically become the object containing the arrow.
  • Confusing local assignment with caller updates: changing a numeric parameter doesn't change the caller's variable.

A practical starting rule: use declarations for named operations, arrows for small callbacks, and method shorthand for receiver-based behavior. Use traditional function expressions when you need a function value with ordinary function semantics.

This tutorial focuses on synchronous functions. Async functions always return promises; they and generators need additional concepts beyond these basics. Continue with our JavaScript async/await example when you're ready, or practice reusable operations inside JavaScript loops.

FAQ

What is the difference between an arrow function and a regular function in JavaScript?

Arrow functions offer concise syntax and capture this and arguments from their enclosing scope. Ordinary functions create their own bindings, and plain traditional functions can be constructors; arrows cannot.

When should I use an arrow function in JavaScript?

Use arrows for concise callbacks and functions that should retain the surrounding this. Prefer ordinary method syntax when this must refer to the object used for the method call.

What is the difference between a function declaration and a function expression?

A function declaration defines a named function that can be called before its declaration within its scope. A function expression creates a function value during expression evaluation; when stored in const or let, its binding cannot be accessed before initialization.

Can an arrow function be used as a constructor?

No. Calling an arrow function with new throws TypeError, and arrows have no own .prototype property by default. Use a class or a constructible traditional function when you need constructor behavior.

What does a JavaScript function return if there is no return statement?

An ordinary synchronous function that reaches the end of its body returns undefined. An expression-bodied arrow implicitly returns its expression's value, even without a written return statement.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me