Sitemap

Functions in JavaScript

14 min readOct 6, 2026

--

What is a function?

A function is a reusable block of code designed to perform a specific task.

Instead of writing the same code repeatedly, we can define it once and call it whenever we need it.

function greet() {             //defination or declaration
console.log("Hello");
}
greet(); //call or invoke
greet();

Here, greet() is called twice, so the same code is executed twice.

The function executes only when we call or invoke it, Functions help make code reusable, organized, and easier to maintain.

Function Declaration

A function declaration is a way of defining a named function directly using the function keyword.

function greet() {
console.log("Hello");
}
greet();

Here, greet is the function name.

Function Expression

A function expression is a function that is created as part of an expression and assigned to a variable.

const greet = function() {
console.log("Hello");
};
greet();

A simple way to remember it:

  • Function declaration → directly define a named function.
  • Function expression → create a function and assign it to a variable.

Anonymous Function

An anonymous function is a function that doesn’t have a name.

const greet = function() {
console.log("Hello");
};

Here, the function itself doesn’t have a name. It is an anonymous function, and we are storing that function in the variable greet.

Anonymous functions are commonly used when we don’t need to give the function its own name, especially when passing a function somewhere as a callback.

Immediately Invoked Function Expression (IIFE)

An IIFE is a function that is executed immediately after it is created.

(function () {
console.log("Hello");
})();

The final () invokes the function immediately, so we don't need to call it separately.

IIFE with an Anonymous Function

IIFEs are commonly written using anonymous functions:

(function () {
console.log("Hello");
})();

The function has no name, and it is immediately invoked.

Why was IIFE commonly used?

One important historical use of IIFEs was to create a private scope and avoid polluting the global scope.

(function () {
const secret = "hello";
console.log(secret);
})();
console.log(secret); // ReferenceError

Here, secret exists only inside the IIFE and cannot be accessed from outside.

IIFEs were especially useful before modern JavaScript features such as let, const, and ES modules provided better ways to control scope.

Today, IIFEs are less necessary for creating private scope, but they are still useful when we want a piece of code to execute immediately within its own scope.

Arrow Functions

Arrow functions provide a shorter syntax for writing functions in JavaScript.

They don’t use the function keyword and use the => syntax instead.

const greet = () => {
console.log("Hello");
};

Arrow functions are commonly used as anonymous functions assigned to variables.

Arrow Function with Parameters

If there are no parameters, we use ():

const greet = () => {
console.log("Hello");
};

With a single parameter, parentheses are optional:

const greet = name => {
console.log("Hello " + name);
};

With multiple parameters, parentheses are required:

const add = (a, b) => {
return a + b;
};

Implicit Return

If an arrow function contains a single expression that needs to be returned, we can omit the {} and the return keyword.

const add = (a, b) => a + b;

This is called an implicit return.

The equivalent function with an explicit return is:

const add = (a, b) => {
return a + b;
};

Returning an Object

When returning an object directly from an arrow function, we wrap the object in parentheses because {} is otherwise interpreted as the function body.

const getUser = () => ({ name: "Pooja" });

Why Arrow Functions Are Different

Arrow functions are not just a shorter syntax for regular functions. They behave differently in a few important ways.

this in Arrow Functions

Arrow functions do not have their own this.

A regular function gets its this based on how it is called.

const user = {
name: "Pooja",
greet: function () {
console.log(this.name);
}
};
user.greet(); // Pooja

Here, this refers to user because the function is called as user.greet().

An arrow function does not create its own this. Instead, it uses this from its surrounding scope.

const user = {
name: "Pooja",
greet: () => {
console.log(this.name);
}
};
user.greet(); //undefined not "Pooja"

This behavior is called lexical this.

One common use is inside callbacks, where we want to preserve the this from the surrounding function:

class User {
name = "Pooja";
greet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
}

The arrow function uses the this from greet().

arguments in Arrow Functions

Regular functions have an arguments object containing the arguments passed to them.

function add() {
console.log(arguments);
}
add(10, 20, 30);

Arrow functions don’t have their own arguments.

For modern JavaScript, we usually use rest parameters instead:

const add = (...args) => {
console.log(args);
};
add(10, 20, 30); // [10, 20, 30]

Arrow Functions Cannot Be Used with new

Regular functions can be used as constructors:

function Person(name) {
this.name = name;
}
const p = new Person("Pooja");

Arrow functions cannot be used with new:

const Person = (name) => {
this.name = name;
};
const p = new Person("Pooja"); // TypeError

Because arrow functions are not constructible, they also don’t have their own prototype property.

So arrow functions are more than just a shorter syntax. Their different behavior around this, arguments, and constructors is an important reason why we use them.

Parameters vs Arguments

Parameters are the variables defined in a function declaration or expression.

Arguments are the actual values passed to the function when it is called.

function greet(name) {
console.log("Hello " + name);
}
greet("Pooja");

Here:

  • name → parameter
  • "Pooja" → argument

A simple way to remember:

Parameter = placeholder
Argument = actual value

A function can have multiple parameters and arguments:

function add(a, b) {
return a + b;
}
add(10, 20);

Here, a and b are parameters, while 10 and 20 are arguments.

Fewer Arguments Than Parameters

If fewer arguments are provided than the number of parameters, the missing parameters get the value undefined.

function add(a, b) {
console.log(a); // 10
console.log(b); // undefined
}
add(10);

Here, a gets 10, but no value is provided for b, so b is undefined.

More Arguments Than Parameters

If more arguments are provided than the number of parameters, JavaScript does not throw an error.

function add(a, b) {
console.log(a, b); // 10 20
}
add(10, 20, 30);

The extra argument 30 is not assigned to a named parameter.

Extra arguments can be accessed using the arguments object in regular functions or using rest parameters:

function add(a, b, ...rest) {
console.log(rest); // [30, 40]
}
add(10, 20, 30, 40);

Rest Parameters and Spread Syntax

Rest and spread both use the ... syntax, but they do opposite things.

  • Rest → collects multiple values into one container.
  • Spread → expands values from a container into individual values.

Rest Parameters

Rest parameters allow a function to collect the remaining arguments into an array.

function add(a, b, ...rest) {
console.log(a); // 10
console.log(b); // 20
console.log(rest); // [30, 40, 50]
}
add(10, 20, 30, 40, 50);

Here, a gets 10, b gets 20, and ...rest collects the remaining arguments into an array.

Rest = collect the remaining values.

Restrictions of Rest Parameters

1. Rest parameter must be the last parameter

function add(a, ...rest) {
// valid
}

This is invalid:

function add(...rest, a) {
// SyntaxError
}

Because rest collects all remaining arguments, nothing can come after it.

2. Only one rest parameter is allowed :

function add(...numbers, ...values) {
// SyntaxError
}

3. Rest parameter must be a parameter, not a standalone variable

This is invalid:

const ...numbers = [1, 2, 3];

But spread syntax can be used there:

const numbers = [1, 2, 3];
const copy = [...numbers];

So the easiest way to remember the main restriction is:

Rest must be the last parameter and there can be only one rest parameter.

Spread Syntax

Spread syntax expands the values of an iterable, such as an array, into individual values.

const numbers = [10, 20, 30];
console.log(...numbers);

This is similar to:

console.log(10, 20, 30);

The array is expanded into individual values.

Spread = expand the values.

Rest vs Spread

Although both use ..., their purpose depends on where they are used:

// Rest → collects values
function add(...numbers) {
console.log(numbers); // [10, 20, 30]
}
add(10, 20, 30);

// Spread → expands values
const numbers = [10, 20, 30];
console.log(...numbers); // 10 20 30

Rest collects… Spread expands.

Practical Uses of Spread

Copying an Array : Spread can be used to create a shallow copy of an array.

const numbers = [1, 2, 3];
const copy = [...numbers];
console.log(copy); // [1, 2, 3]

Combining Arrays: Spread makes it easy to combine multiple arrays.

const numbers1 = [1, 2, 3];
const numbers2 = [4, 5, 6];
const combined = [...numbers1, ...numbers2];
console.log(combined); // [1, 2, 3, 4, 5, 6]

Adding Elements to an Array: We can also add new elements while spreading an existing array.

const numbers = [2, 3, 4];
const newNumbers = [1, ...numbers, 5];
console.log(newNumbers); // [1, 2, 3, 4, 5]

Copying and Updating Objects: Spread can also be used with objects to create a shallow copy and add or update properties.

const user = {
name: "Pooja",
age: 25
};
const updatedUser = {
...user,
age: 26
};
console.log(updatedUser); // { name: "Pooja", age: 26 }

Here, the properties of user are spread into the new object, and age is updated to 26.

Default Parameters

A default parameter means we can provide a default value for a parameter, which JavaScript will use when the caller doesn’t provide a value (or provides undefined).

Basic Example

function greet(name = "Guest") {
console.log(name);
}
greet("Pooja"); // Pooja
greet(); // Guest

Here:

name = "Guest"

is the default parameter.

When we call:

greet();

name is undefined, so JavaScript uses "Guest".

Important Interview Point

The default value is used when the argument is undefined, not for every falsy value.

function greet(name = "Guest") {
console.log(name);
}
greet(undefined); // Guest
greet(null); // null
greet(""); // ""
greet(0); // 0

So don’t say:

“If the argument is falsy, the default parameter is used.”

That’s not correct.

Default Parameters with Multiple Parameters

function add(a, b = 10) {
return a + b;
}
add(5); // 15
add(5, 20); // 25

If b isn't supplied, it gets the default value 10.

Default parameters can be used with both regular functions and arrow functions:

const greet = (name = "Guest") => {
console.log(name);
};

Return Values

A return statement is used when a function needs to return a value to its caller.

function add(a, b) {
return a + b; //produces and returns 30
}
const result = add(10, 20);
console.log(result); // 30

return gives a value back from the function.

return vs console.log()

This is an important distinction. console.log()prints a value, while return gives the value back to the caller.

function add(a, b) {
console.log(a + b);
}
const result = add(10, 20);
console.log(result);

Output:

30
undefined

The function printed 30, but it didn't return anything. Therefore, result receives undefined.

Get Pooja Chapole’s stories in your inbox

Join Medium for free to get updates from this writer.

Compare this with:

function add(a, b) {
return a + b;
}
const result = add(10, 20);
console.log(result); // 30

Now the returned value can be stored, reused, or passed to another function.

A Function Can Return Different Types of Values

function getAge() {
return 25; //return a number
}
function getName() {
return "Pooja"; //return a string
}
function getNumbers() {
return [1, 2, 3]; // return a array
}
function getUser() {
return {
name: "Pooja",
age: 25
}; // return a object
}

return Stops Function Execution

When a return statement executes, the function immediately finishes.

function test() {
return 10;
console.log("Hello"); //never executes
}

If There Is No return

If a function doesn’t explicitly return a value, it returns undefined.

function greet() {
console.log("Hello");
}
const result = greet();
console.log(result); // undefined

return; also returns undefined:

function test() {
return;
}
console.log(test()); // undefined

Key Takeaway

A return value is the value a function sends back to its caller using the return statement. If a function doesn't explicitly return a value, it returns undefined.

First-Class Functions

JavaScript treats functions as first-class values.

This means functions can be treated like other values. For example, you can:

  • Store a function in a variable
  • Pass a function as an argument
  • Return a function from another function
  • Store functions in objects or arrays
const greet = function() {
console.log("Hello");
};

Here, the function is stored in the greet variable.

You can also pass it as an argument:

function execute(fn) {
fn();
}
execute(greet);

Or return it from another function:

function getFunction() {
return function() {
console.log("Hello");
};
}

So:

First-class function is a characteristic of the language: functions can be treated as values.

Callback Functions

A callback function is a function that is passed to another function, so that the receiving function can invoke it.

function greet() {
console.log("Hello");
}
function execute(fn) {
fn();
}
execute(greet);

Here:

  • greet → callback function
  • execute → receives the callback
  • fn() → invokes the callback

A common example is setTimeout():

setTimeout(() => {
console.log("Hello");
}, 1000);

The arrow function passed to setTimeout() is a callback.

Array methods also commonly use callbacks:

const numbers = [1, 2, 3];
numbers.map((num) => {
return num * 2;
});

The function passed to map() is a callback.

Higher-Order Functions

A higher-order function (HOF) is a function that does at least one of these:

  1. Accepts a function as an argument
  2. Returns a function

For example:

function execute(fn) {
fn();
}

execute is a higher-order function because it accepts a function as an argument.

A function can also be higher-order by returning another function:

function multiplyBy(x) {
return function(y) {
return x * y;
};
}

Here, multiplyBy is a higher-order function because it returns a function.

How They Relate

Consider:

function execute(fn) {
fn();
}
function greet() {
console.log("Hello");
}
execute(greet);

greet

  • Is a first-class function because JavaScript allows functions to be treated as values.
  • Is a callback because it is passed to execute().

execute

  • Is a higher-order function because it accepts a function.

These are not three different types of functions. They describe different properties or roles:

First-class function
↓
Functions can be treated as values

Callback function
↓
A function passed to another function

Higher-order function
↓
A function that accepts or returns another function

Important Distinction

Don’t say:

“Every first-class function is a callback.”

That’s not true.

const greet = function() {
console.log("Hello");
};

greet is a first-class function. It becomes a callback when it is passed to another function.

Similarly, a higher-order function doesn’t necessarily have to receive a callback. It can simply return another function.

Closures in JavaScript

A closure is created when a function remembers and can access variables from its surrounding lexical scope, even after that outer scope has finished executing.

Basic Example

function outer() {
let message = "Hello";
function inner() {
console.log(message);
}
return inner;
}
const greet = outer();
greet(); // Hello

Let’s understand what happens here.

When outer() executes:

let message = "Hello";

creates the variable message.

Then inner() is created inside outer(): The inner function uses the message variable from its outer scope.

Then outer() returns inner:

return inner;

So:

const greet = outer();

Now greet holds the inner function.

Even though outer() has finished executing, greet() can still access message:

greet(); // Hello

This preserved access to message is the closure.

Why Is This Possible?

Normally, you might think that once outer() finishes, its local variables should no longer be accessible.

function outer() {
let message = "Hello";
}
outer();
console.log(message); // ReferenceError

message is local to outer(), so it cannot be accessed from outside.

But when an inner function still references that variable, the function retains access to it:

function outer() {
let message = "Hello";
return function inner() {
console.log(message);
};
}
const greet = outer();
greet(); // Hello

The inner function forms a closure over message.

Closures Can Preserve Changing Values

A closure doesn’t just remember the initial value. It can access the same variable and observe changes to it.

function createCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3

What’s happening?

count starts at:

let count = 0;

Every time counter() is called: count++ changes the same count variable.

The closure keeps access to that variable between function calls.

Closures Can Create Private State

One of the most common uses of closures is creating private state.

function createCounter() {
let count = 0;
return {
increment() {
count++;
},
getCount() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
counter.increment();

console.log(counter.getCount()); // 2

Notice that we cannot directly access count:

console.log(counter.count); // undefined

count is private to the createCounter() scope.

But the returned methods have access to it because of closures.

This gives us a simple way to control how a variable can be accessed or modified.

Each Closure Can Have Its Own State

This is an important property of closures.

function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1
console.log(counter2()); // 2

counter1 and counter2 don't share the same count.

Each call to: createCounter()creates a new execution context with its own count.

Therefore:

counter1 → count = 2
counter2 → count = 2

They maintain their own independent state.

Closures and Lexical Scope

Closures are closely related to lexical scope.

const name = "Pooja";
function greet() {
console.log(name);
}
greet(); // Pooja

The function can access name because of where the function was defined, not where it is called, this is lexical scoping.

Closures build on this idea: when a function continues to exist outside its original scope, it retains access to the variables it could access from that scope.

Closure with Function Parameters

Parameters can also be captured by a closure.

function multiplyBy(x) {
return function(y) {
return x * y;
};
}
const multiplyByTwo = multiplyBy(2);
console.log(multiplyByTwo(5)); // 10
console.log(multiplyByTwo(10)); // 20

Here, x belongs to the multiplyBy() function.

The returned function remembers x:

return function(y) {
return x * y;
};

So even after multiplyBy() finishes, the returned function can still access x. this is a closure.

Closure with setTimeout

Closures are also commonly encountered with asynchronous code.

function greetAfterDelay(name) {
setTimeout(function() {
console.log("Hello " + name);
}, 1000);
}
greetAfterDelay("Pooja");

The callback passed to setTimeout() uses the name parameter.

Even though greetAfterDelay() finishes executing before the callback runs, the callback still has access to name.

That’s possible because of a closure.

Closure with Loops

Closures are especially important when dealing with loops and asynchronous callbacks.

Using let:

for (let i = 1; i <= 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}

Output:

1
2
3

Each iteration gets its own block-scoped i, and each callback closes over the appropriate value.

This is one reason understanding scope + closures together is important in JavaScript.

A Common Interview Question

Consider:

function outer() {
let count = 0;
  return function() {
count++;
return count;
};
}
const counter = outer();console.log(counter());
console.log(counter());
console.log(counter());

What is the output?

1
2
3

Why?

Because the returned function forms a closure over count.

The same count variable is preserved between calls:

outer()
↓
count = 0
↓
returned function remembers count
↓
counter() → count = 1
↓
counter() → count = 2
↓
counter() → count = 3

Closure vs Scope

These concepts are related but not identical.

Scope answers:

Where can this variable be accessed?

Closure answers:

How can a function continue accessing variables from its surrounding scope even after that scope has finished executing?

For example:

function outer() {
let message = "Hello";
return function inner() {
console.log(message);
};
}

message is accessible to inner() because of lexical scope.

When inner() is returned and used later, its continued access to message is the closure.

Important Interview Points

Remember these points:

1. A closure is not necessarily two functions

You don’t define a closure by counting functions.

The important part is:

A function retains access to variables from its surrounding lexical scope.

2. The inner function doesn’t copy the variable

It retains access to the variable itself.

That’s why changes persist:

function counter() {
let count = 0;
return function() {
count++;
return count;
};
}

The returned function continues to work with the same count.

3. Closures can preserve state

This is why closures are useful for counters, private data, function factories, and callbacks.

4. Closures are created naturally

You don’t need a special closure keyword.

Whenever a function retains access to variables from its surrounding lexical scope, a closure is involved.

Key Takeaway

A closure is a function together with its preserved access to variables from its surrounding lexical scope.

The most important example to remember is:

function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Here, the returned function remembers count even after createCounter() has finished executing. That preserved access is the closure.

--

--