Functions in JavaScript
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); // ReferenceErrorHere, 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(); // PoojaHere, 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"); // TypeErrorBecause arrow functions are not constructible, they also don’t have their own
prototypeproperty.
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(); // GuestHere:
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); // 0So 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); // 25If 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
returngives 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
undefinedThe 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); // 30Now 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
returnstatement 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); // undefinedreturn; also returns undefined:
function test() {
return;
}
console.log(test()); // undefinedKey Takeaway
A return value is the value a function sends back to its caller using the
returnstatement. If a function doesn't explicitly return a value, it returnsundefined.
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 functionexecute→ receives the callbackfn()→ 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:
- Accepts a function as an argument
- 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 functionImportant 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(); // HelloLet’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(); // HelloThis 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); // ReferenceErrormessage 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(); // HelloThe 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(); // 3What’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()); // 2Notice that we cannot directly access count:
console.log(counter.count); // undefinedcount 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()); // 2counter1 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 = 2They 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(); // PoojaThe 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)); // 20Here, 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
3Each 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
3Why?
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 = 3Closure 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()); // 3Here, the returned function remembers count even after createCounter() has finished executing. That preserved access is the closure.




