Functions in JavaScript

Functions are one of the fundamental building blocks in JavaScript. They are reusable blocks of code designed to perform a particular task. Functions allow you to structure your code, make it more readable, reusable, and maintainable.

Function Fundamentals

A function is a set of statements that performs a task or calculates a value. To use a function, you must define it somewhere in the scope from which you wish to call it.

// Basic function declaration
function greet() {
console.log("Hello, world!");
}

// Calling the function
greet(); // Outputs: "Hello, world!"

// Function with parameters
function greetPerson(name) {
console.log("Hello, " + name + "!");
}

greetPerson("Alice"); // Outputs: "Hello, Alice!"

// Function with return value
function sum(a, b) {
return a + b;
}

let result = sum(5, 3); // result = 8
console.log(result); // Outputs: 8

Function Declarations vs. Function Expressions

There are several ways to define functions in JavaScript. The two most common are function declarations and function expressions.

// Function Declaration
function multiply(a, b) {
return a * b;
}

// Function Expression
const divide = function(a, b) {
return a / b;
};

// Usage
console.log(multiply(4, 5)); // Outputs: 20
console.log(divide(20, 4)); // Outputs: 5

// The key difference: Function declarations are hoisted
console.log(subtract(10, 5)); // Works! Outputs: 5

function subtract(a, b) {
return a - b;
}

// But function expressions are not hoisted
// console.log(power(2, 3)); // Error: power is not a function

const power = function(base, exponent) {
return Math.pow(base, exponent);
};

Arrow Functions

Arrow functions were introduced in ES6 (ECMAScript 2015) and provide a more concise syntax for writing functions. They also have some special behavior regarding the this keyword.

// Traditional function expression
const square = function(x) {
return x * x;
};

// Arrow function equivalent
const squareArrow = (x) => {
return x * x;
};

// Even more concise for single expressions
const squareConcise = x => x * x;

console.log(square(5)); // Outputs: 25
console.log(squareArrow(5)); // Outputs: 25
console.log(squareConcise(5)); // Outputs: 25

// Arrow functions with multiple parameters
const add = (a, b) => a + b;
console.log(add(3, 7)); // Outputs: 10

// Arrow functions with no parameters
const sayHello = () => "Hello!";
console.log(sayHello()); // Outputs: Hello!

// Arrow functions returning objects (need parentheses)
const createPerson = (name, age) => ({ name: name, age: age });
const person = createPerson("John", 30);
console.log(person); // Outputs: { name: "John", age: 30 }

Function Parameters

JavaScript functions have flexible parameter handling capabilities, including default parameters, rest parameters, and the arguments object.

// Default parameters (ES6)
function greet(name = "Guest") {
return `Hello, ${name}!`;
}

console.log(greet("Alice")); // Outputs: Hello, Alice!
console.log(greet()); // Outputs: Hello, Guest!

// Multiple default parameters
function createProfile(name = "Anonymous", age = 0, country = "Unknown") {
return `Name: ${name}, Age: ${age}, Country: ${country}`;
}

console.log(createProfile("Bob", 25, "USA")); // Outputs: Name: Bob, Age: 25, Country: USA
console.log(createProfile("Charlie")); // Outputs: Name: Charlie, Age: 0, Country: Unknown

// Rest parameters (ES6)
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2)); // Outputs: 3
console.log(sum(1, 2, 3, 4, 5)); // Outputs: 15

// Combining regular and rest parameters
function displayTeam(teamName, ...members) {
console.log(`Team: ${teamName}`);
console.log(`Members: ${members.join(", ")}`);
}

displayTeam("Avengers", "Iron Man", "Captain America", "Thor");
// Outputs:
// Team: Avengers
// Members: Iron Man, Captain America, Thor

// The arguments object (older approach)
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}

console.log(oldSum(1, 2, 3, 4)); // Outputs: 10

Scope and Closures

Understanding scope and closures is crucial for mastering JavaScript functions. Scope determines the accessibility of variables, and closures allow functions to retain access to variables from their parent scope.

// Global scope
const globalVar = "I'm global";

function outerFunction() {
// Function scope
const outerVar = "I'm from outer function";

function innerFunction() {
// Inner function scope
const innerVar = "I'm from inner function";

console.log(innerVar); // Accessible
console.log(outerVar); // Accessible (from parent scope)
console.log(globalVar); // Accessible (from global scope)
}

innerFunction();
console.log(outerVar); // Accessible
// console.log(innerVar); // Error: innerVar is not defined
}

outerFunction();
console.log(globalVar); // Accessible
// console.log(outerVar); // Error: outerVar is not defined

// Closures
function createCounter() {
let count = 0; // Private variable

return function() {
count++; // Accessing the parent scope's variable
return count;
};
}

const counter = createCounter();
console.log(counter()); // Outputs: 1
console.log(counter()); // Outputs: 2
console.log(counter()); // Outputs: 3

// Another counter is independent
const counter2 = createCounter();
console.log(counter2()); // Outputs: 1

// Practical closure example: Creating private methods
function createBankAccount(initialBalance) {
let balance = initialBalance;

return {
deposit: function(amount) {
balance += amount;
return `Deposited ${amount}. New balance: ${balance}`;
},
withdraw: function(amount) {
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
return `Withdrew ${amount}. New balance: ${balance}`;
},
getBalance: function() {
return `Current balance: ${balance}`;
}
};
}

const account = createBankAccount(100);
console.log(account.getBalance()); // Outputs: Current balance: 100
console.log(account.deposit(50)); // Outputs: Deposited 50. New balance: 150
console.log(account.withdraw(30)); // Outputs: Withdrew 30. New balance: 120
// balance is not directly accessible
// console.log(account.balance); // Outputs: undefined

Higher-Order Functions

Higher-order functions are functions that operate on other functions, either by taking them as arguments or by returning them. They are a powerful feature of JavaScript and are widely used in functional programming.

// Function that takes another function as an argument
function operate(a, b, operation) {
return operation(a, b);
}

// Functions to pass as arguments
function add(x, y) { return x + y; }
function subtract(x, y) { return x - y; }
function multiply(x, y) { return x * y; }
function divide(x, y) { return x / y; }

// Using the higher-order function
console.log(operate(10, 5, add)); // Outputs: 15
console.log(operate(10, 5, subtract)); // Outputs: 5
console.log(operate(10, 5, multiply)); // Outputs: 50
console.log(operate(10, 5, divide)); // Outputs: 2

// Function that returns another function
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // Outputs: 10
console.log(triple(5)); // Outputs: 15

// Array methods that use higher-order functions
const numbers = [1, 2, 3, 4, 5];

// map: transforms each element
const squared = numbers.map(num => num * num);
console.log(squared); // Outputs: [1, 4, 9, 16, 25]

// filter: selects elements that match a condition
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Outputs: [2, 4]

// reduce: accumulates values
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // Outputs: 15

// forEach: performs an action for each element
numbers.forEach(num => console.log(`Number: ${num}`));
// Outputs:
// Number: 1
// Number: 2
// Number: 3
// Number: 4
// Number: 5

Immediately Invoked Function Expressions (IIFE)

An IIFE is a JavaScript function that runs as soon as it is defined. It's a design pattern used to create a new scope, avoid polluting the global namespace, and execute code immediately.

// Basic IIFE syntax
(function() {
console.log("This function runs immediately!");
})();

// IIFE with parameters
(function(name) {
console.log(`Hello, ${name}!`);
})("World");

// IIFE that returns a value
const result = (function() {
const a = 5;
const b = 10;
return a + b;
})();

console.log(result); // Outputs: 15

// IIFE for creating private scope
const counter = (function() {
let count = 0; // Private variable

return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getValue: function() {
return count;
}
};
})();

console.log(counter.getValue()); // Outputs: 0
console.log(counter.increment()); // Outputs: 1
console.log(counter.increment()); // Outputs: 2
console.log(counter.decrement()); // Outputs: 1
// count is not accessible
// console.log(count); // Error: count is not defined

// Modern alternative using block scope and const/let
{
const privateVar = "I am private";
console.log(privateVar); // Accessible inside the block
}
// console.log(privateVar); // Error: privateVar is not defined

Function Methods: call, apply, and bind

JavaScript functions are objects, and they come with special methods that allow you to control how they are executed, particularly with respect to the this keyword.

// Object with a method
const person = {
firstName: "John",
lastName: "Doe",
fullName: function() {
return this.firstName + " " + this.lastName;
}
};

console.log(person.fullName()); // Outputs: John Doe

// Another object
const anotherPerson = {
firstName: "Jane",
lastName: "Smith"
};

// Using call to borrow the fullName method
console.log(person.fullName.call(anotherPerson)); // Outputs: Jane Smith

// Function with parameters
function greet(greeting, punctuation) {
return greeting + ", " + this.firstName + punctuation;
}

// Using call with arguments
console.log(greet.call(person, "Hello", "!")); // Outputs: Hello, John!

// Using apply (similar to call but takes an array of arguments)
console.log(greet.apply(anotherPerson, ["Hi", "!"])); // Outputs: Hi, Jane!

// Using bind to create a new function with a fixed 'this'
const greetJohn = greet.bind(person);
console.log(greetJohn("Hey", "!")); // Outputs: Hey, John!

// Bind with preset parameters
const sayHelloToJohn = greet.bind(person, "Hello");
console.log(sayHelloToJohn("!!")); // Outputs: Hello, John!!

// Practical example: Event handlers
const button = {
content: "Click Me",
click() {
console.log(`Button ${this.content} clicked`);
}
};

// Simulating a DOM event that loses 'this' context
const simulateEvent = function(callback) {
// 'this' is not button here
callback();
};

// This would fail because 'this' is not button
// simulateEvent(button.click); // Error: Cannot read property 'content' of undefined

// Using bind to fix the context
simulateEvent(button.click.bind(button)); // Outputs: Button Click Me clicked

Recursive Functions

A recursive function is a function that calls itself until it reaches a base case. Recursion is useful for tasks that can be broken down into similar subtasks.

// Factorial calculation using recursion
function factorial(n) {
// Base case
if (n <= 1) {
return 1;
}
// Recursive case
return n * factorial(n - 1);
}

console.log(factorial(5)); // Outputs: 120 (5 * 4 * 3 * 2 * 1)

// Fibonacci sequence using recursion
function fibonacci(n) {
// Base cases
if (n <= 0) return 0;
if (n === 1) return 1;

// Recursive case
return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(7)); // Outputs: 13

// Note: Simple recursive fibonacci is inefficient
// Improved version with memoization
function efficientFibonacci(n, memo = {}) {
if (n in memo) return memo[n];
if (n <= 0) return 0;
if (n === 1) return 1;

memo[n] = efficientFibonacci(n - 1, memo) + efficientFibonacci(n - 2, memo);
return memo[n];
}

console.log(efficientFibonacci(50)); // Much faster for large numbers

// Recursive function to traverse a nested object
function traverseObject(obj, indent = '') {
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
console.log(`${indent}${key}:`);
traverseObject(obj[key], indent + ' ');
} else {
console.log(`${indent}${key}: ${obj[key]}`);
}
}
}

const nestedObj = {
name: "John",
age: 30,
address: {
street: "123 Main St",
city: "Anytown",
country: "USA"
},
hobbies: ["reading", "coding", "hiking"]
};

traverseObject(nestedObj);
// Outputs:
// name: John
// age: 30
// address:
// street: 123 Main St
// city: Anytown
// country: USA
// hobbies: reading,coding,hiking

Generator Functions

Generator functions are a special type of function introduced in ES6 that can be paused and resumed, allowing for the creation of iterators in a more elegant way.

// Basic generator function
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}

const generator = simpleGenerator();

console.log(generator.next()); // Outputs: { value: 1, done: false }
console.log(generator.next()); // Outputs: { value: 2, done: false }
console.log(generator.next()); // Outputs: { value: 3, done: false }
console.log(generator.next()); // Outputs: { value: undefined, done: true }

// Using a generator to create an infinite sequence
function* idGenerator() {
let id = 1;
while (true) {
yield id++;
}
}

const ids = idGenerator();
console.log(ids.next().value); // Outputs: 1
console.log(ids.next().value); // Outputs: 2
console.log(ids.next().value); // Outputs: 3

// Generator with parameters
function* powerGenerator(base) {
let exponent = 0;
while (true) {
yield Math.pow(base, exponent++);
}
}

const powersOfTwo = powerGenerator(2);
console.log(powersOfTwo.next().value); // Outputs: 1 (2^0)
console.log(powersOfTwo.next().value); // Outputs: 2 (2^1)
console.log(powersOfTwo.next().value); // Outputs: 4 (2^2)
console.log(powersOfTwo.next().value); // Outputs: 8 (2^3)

// Using generators for iteration
function* iterateArray(array) {
for (let i = 0; i < array.length; i++) {
yield array[i];
}
}

const fruits = ['apple', 'banana', 'orange'];
const fruitIterator = iterateArray(fruits);

for (const fruit of fruitIterator) {
console.log(fruit);
}
// Outputs:
// apple
// banana
// orange

// Generator delegation using yield*
function* mainGenerator() {
yield 'A';
yield* subGenerator();
yield 'D';
}

function* subGenerator() {
yield 'B';
yield 'C';
}

const letters = mainGenerator();
for (const letter of letters) {
console.log(letter);
}
// Outputs:
// A
// B
// C
// D

Async Functions

Async functions, introduced in ES2017, provide a cleaner way to work with asynchronous code, making it look and behave more like synchronous code.

// Basic Promise example
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data fetched successfully");
}, 1000);
});
}

// Using Promises
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));

// Using async/await
async function getData() {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}

getData();

// Multiple async operations
function fetchUser() {
return new Promise(resolve => {
setTimeout(() => resolve({ id: 1, name: "John" }), 500);
});
}

function fetchPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve([
{ id: 1, title: "Post 1", userId },
{ id: 2, title: "Post 2", userId }
]), 500);
});
}

// Using Promises (chain)
fetchUser()
.then(user => {
console.log("User:", user);
return fetchPosts(user.id);
})
.then(posts => {
console.log("Posts:", posts);
})
.catch(error => console.error(error));

// Using async/await (cleaner)
async function getUserAndPosts() {
try {
const user = await fetchUser();
console.log("User:", user);

const posts = await fetchPosts(user.id);
console.log("Posts:", posts);
} catch (error) {
console.error(error);
}
}

getUserAndPosts();

// Parallel async operations
async function getMultipleData() {
try {
// Run both promises in parallel
const [user, settings] = await Promise.all([
fetchUser(),
new Promise(resolve => setTimeout(() => resolve({ theme: "dark" }), 800))
]);

console.log("User:", user);
console.log("Settings:", settings);
} catch (error) {
console.error(error);
}
}

getMultipleData();

Function Composition and Currying

Function composition and currying are advanced functional programming techniques that allow you to combine functions and create more flexible, reusable code.

// Function composition
function addTwo(x) {
return x + 2;
}

function multiplyByThree(x) {
return x * 3;
}

function subtractTen(x) {
return x - 10;
}

// Manual composition
const result = subtractTen(multiplyByThree(addTwo(5)));
console.log(result); // Outputs: 11 ((5 + 2) * 3 - 10)

// Compose utility function (right to left)
function compose(...functions) {
return function(x) {
return functions.reduceRight((acc, fn) => fn(acc), x);
};
}

const calculate = compose(subtractTen, multiplyByThree, addTwo);
console.log(calculate(5)); // Outputs: 11

// Pipe utility function (left to right)
function pipe(...functions) {
return function(x) {
return functions.reduce((acc, fn) => fn(acc), x);
};
}

const calculateInOrder = pipe(addTwo, multiplyByThree, subtractTen);
console.log(calculateInOrder(5)); // Outputs: 11

// Currying
function multiply(a, b) {
return a * b;
}

// Manual currying
function curriedMultiply(a) {
return function(b) {
return a * b;
};
}

const double = curriedMultiply(2);
const triple = curriedMultiply(3);

console.log(double(5)); // Outputs: 10
console.log(triple(5)); // Outputs: 15

// Generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}

const curriedAdd = curry((a, b, c) => a + b + c);

console.log(curriedAdd(1, 2, 3)); // Outputs: 6
console.log(curriedAdd(1)(2, 3)); // Outputs: 6
console.log(curriedAdd(1, 2)(3)); // Outputs: 6
console.log(curriedAdd(1)(2)(3)); // Outputs: 6

// Practical example: Creating a configurable formatter
const formatNumber = curry((separator, decimals, number) => {
return number.toFixed(decimals).replace('.', separator);
});

const formatEuropean = formatNumber(',', 2);
const formatUS = formatNumber('.', 2);

console.log(formatEuropean(1234.5)); // Outputs: 1234,50
console.log(formatUS(1234.5)); // Outputs: 1234.50

Best Practices for Functions

Following these best practices will help you write cleaner, more maintainable, and more efficient JavaScript functions.

  • Keep functions small and focused - Each function should do one thing and do it well.
  • Use descriptive names - Function names should clearly describe what they do.
  • Limit the number of parameters - If a function needs many parameters, consider using an options object.
  • Return early - Use early returns to avoid deep nesting and improve readability.
  • Avoid side effects - Functions should ideally not modify variables outside their scope.
  • Use pure functions when possible - Functions that always return the same output for the same input are easier to test and reason about.
  • Document your functions - Use JSDoc comments to describe parameters, return values, and behavior.
  • Handle errors appropriately - Use try/catch blocks or return error objects rather than throwing exceptions.
  • Test your functions - Write unit tests to ensure your functions work as expected.
// Bad: Function does too much
function processUser(user) {
// Validate user
if (!user.name || !user.email) {
console.error("Invalid user");
return false;
}

// Update database
database.save(user);

// Send email
sendEmail(user.email, "Welcome!", "Welcome to our platform!");

return true;
}

// Good: Functions are small and focused
function validateUser(user) {
return Boolean(user.name && user.email);
}

function saveUser(user) {
return database.save(user);
}

function sendWelcomeEmail(user) {
return sendEmail(user.email, "Welcome!", "Welcome to our platform!");
}

function processUser(user) {
if (!validateUser(user)) {
console.error("Invalid user");
return false;
}

saveUser(user);
sendWelcomeEmail(user);

return true;
}

// Bad: Too many parameters
function createUser(name, email, age, country, city, street, zipCode, isAdmin) {
// Create user...
}

// Good: Use an options object
function createUser(userData) {
const { name, email, age, address, isAdmin = false } = userData;
// Create user...
}

// Usage
createUser({
name: "John Doe",
email: "john@example.com",
age: 30,
address: {
country: "USA",
city: "New York",
street: "Broadway",
zipCode: "10001"
},
isAdmin: true
});

// Bad: Deep nesting
function processPayment(payment) {
if (payment) {
if (payment.amount > 0) {
if (payment.method) {
// Process payment...
return true;
} else {
return false;
}
} else {
return false;
}
} else {
return false;
}
}

// Good: Early returns
function processPayment(payment) {
if (!payment) return false;
if (payment.amount <= 0) return false;
if (!payment.method) return false;

// Process payment...
return true;
}

// Good: JSDoc comments
/**
* Calculates the total price including tax
* @param {number} price - The base price
* @param {number} taxRate - The tax rate as a decimal (e.g., 0.1 for 10%)
* @returns {number} The total price including tax
*/
function calculateTotalPrice(price, taxRate) {
return price * (1 + taxRate);
}

Common Pitfalls and How to Avoid Them

Even experienced JavaScript developers can fall into these common traps when working with functions. Here's how to recognize and avoid them.

// Pitfall 1: Forgetting to return a value
function add(a, b) {
a + b; // Oops, no return statement!
}

console.log(add(2, 3)); // Outputs: undefined

// Fix: Always include a return statement
function addFixed(a, b) {
return a + b;
}

// Pitfall 2: Unexpected 'this' binding
const user = {
name: "John",
greet: function() {
console.log(`Hello, my name is ${this.name}`);
}
};

const greetFunction = user.greet;
// greetFunction(); // Error: Cannot read property 'name' of undefined

// Fix: Use bind, arrow functions, or keep the method reference
const boundGreet = user.greet.bind(user);
boundGreet(); // Outputs: Hello, my name is John

// Pitfall 3: Modifying parameters
function updateArray(arr) {
arr.push(4); // This modifies the original array!
return arr;
}

const numbers = [1, 2, 3];
updateArray(numbers);
console.log(numbers); // Outputs: [1, 2, 3, 4] (original array is modified)

// Fix: Create a copy before modifying
function updateArrayFixed(arr) {
const copy = [...arr]; // Create a copy using spread operator
copy.push(4);
return copy;
}

const originalNumbers = [1, 2, 3];
const newNumbers = updateArrayFixed(originalNumbers);
console.log(originalNumbers); // Outputs: [1, 2, 3] (original unchanged)
console.log(newNumbers); // Outputs: [1, 2, 3, 4]

// Pitfall 4: Infinite recursion
function countDown(n) {
console.log(n);
countDown(n - 1); // Oops, no base case!
}

// Fix: Always include a base case
function countDownFixed(n) {
console.log(n);
if (n > 0) {
countDownFixed(n - 1);
}
}

// Pitfall 5: Callback hell
getUser(userId, function(user) {
getPosts(user.id, function(posts) {
getComments(posts[0].id, function(comments) {
getAuthor(comments[0].authorId, function(author) {
// Deeply nested and hard to read
console.log(author);
});
});
});
});

// Fix: Use Promises or async/await
async function getUserData(userId) {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
const author = await getAuthor(comments[0].authorId);
console.log(author);
}

// Pitfall 6: Variable hoisting confusion
function example() {
console.log(x); // Outputs: undefined (not an error!)
var x = 5;
}

// Fix: Declare variables at the top or use let/const
function exampleFixed() {
let x; // Declare first
console.log(x); // Outputs: undefined (but at least it's clear)
x = 5;
}

function exampleBetter() {
// This would cause a ReferenceError, making the issue more obvious
// console.log(y);
// let y = 5;

// Better approach: initialize before use
let y = 5;
console.log(y); // Outputs: 5
}

Practice Exercise

Let's put your knowledge of JavaScript functions to the test with this practice exercise.

/*
Exercise: Create a Shopping Cart Module

Requirements:
1. Create a shopping cart module using closures
2. The cart should be able to add items, remove items, update quantities, and calculate the total
3. Each item should have: id, name, price, and quantity
4. The cart should maintain privacy of its data
5. Implement the following methods:
- addItem(id, name, price, quantity)
- removeItem(id)
- updateQuantity(id, quantity)
- getTotal()
- getCartContents()
*/

// Your solution here
function createShoppingCart() {
// Private data
const items = [];

// Helper function to find item index
function findItemIndex(id) {
return items.findIndex(item => item.id === id);
}

// Public API
return {
addItem(id, name, price, quantity = 1) {
const index = findItemIndex(id);

if (index !== -1) {
// Item exists, update quantity
items[index].quantity += quantity;
} else {
// Add new item
items.push({ id, name, price, quantity });
}

return this; // For method chaining
},

removeItem(id) {
const index = findItemIndex(id);

if (index !== -1) {
items.splice(index, 1);
return true;
}

return false;
},

updateQuantity(id, quantity) {
if (quantity <= 0) {
return this.removeItem(id);
}

const index = findItemIndex(id);

if (index !== -1) {
items[index].quantity = quantity;
return true;
}

return false;
},

getTotal() {
return items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
},

getCartContents() {
// Return a copy to prevent direct modification
return [...items];
},

getItemCount() {
return items.reduce((count, item) => count + item.quantity, 0);
},

clearCart() {
items.length = 0;
return this;
}
};
}

// Usage example
const cart = createShoppingCart();

cart.addItem(1, "Laptop", 999.99, 1);
cart.addItem(2, "Mouse", 29.99, 2);
cart.addItem(3, "Keyboard", 59.99, 1);

console.log("Cart contents:", cart.getCartContents());
console.log("Total: $" + cart.getTotal().toFixed(2));

cart.updateQuantity(2, 3); // Update Mouse quantity to 3
console.log("Updated cart total: $" + cart.getTotal().toFixed(2));

cart.removeItem(3); // Remove Keyboard
console.log("Items after removal:", cart.getCartContents());
console.log("Item count:", cart.getItemCount());

cart.clearCart();
console.log("Cart after clearing:", cart.getCartContents());

Key Points to Remember

  • Functions are first-class objects in JavaScript, which means they can be assigned to variables, passed as arguments, and returned from other functions.
  • There are multiple ways to define functions: function declarations, function expressions, and arrow functions, each with its own characteristics.
  • Arrow functions provide a more concise syntax and lexical this binding.
  • Closures allow functions to retain access to variables from their parent scope, even after the parent function has executed.
  • Higher-order functions take functions as arguments or return functions, enabling powerful functional programming patterns.
  • The call, apply, and bind methods allow you to control the this value in function execution.
  • Async functions and generators provide elegant solutions for asynchronous programming and iteration.
  • Function composition and currying are advanced techniques that enhance code reusability and modularity.
  • Following best practices like keeping functions small, using descriptive names, and avoiding side effects leads to more maintainable code.

Further Learning Resources