< / >
Published on

Functions

Table of Contents

Functions

Functions in JavaScript are blocks of reusable code that can be executed when called upon. They are a fundamental building block of JavaScript programming and are used to encapsulate a set of instructions.

Example:

// Function declaration
function greet(name) {
  console.log(`Hello, ${name}!`)
}

// Function call
greet('Rajnish') // Output: Hello, Rajnish!

In this example, greet is a function that takes a name parameter and logs a greeting message to the console.

Function Declaration vs. Function Expression

Function Declaration

Function declarations define named functions. They are hoisted to the top of their scope, meaning they can be called before they are declared in the code.

Example:

greet('Rajnish') // Output: Hello, Rajnish!

function greet(name) {
  console.log(`Hello, ${name}!`)
}

Function Expression

Function expressions, on the other hand, define functions as part of an expression. They are not hoisted and cannot be called before they are defined.

Example:

const callGreet = function greet(name) {
  console.log(`Hello, ${name}!`)
}

callGreet('Rajnish') // Output: Hello, Rajnish!

Arrow Functions

Arrow functions are a more concise way to write function expressions in JavaScript. They have a shorter syntax compared to traditional function expressions and lexically bind the this value.

Example:

const callGreet = (name) => {
  console.log(`Hello, ${name}!`)
}

callGreet('Rajnish') // Output: Hello, Rajnish!

Scope and Closures

Scope refers to the visibility and accessibility of variables within a particular context in JavaScript. Closures occur when a function is able to remember and access its lexical scope even when it is executed outside of that scope.

Example:

function outer() {
  let outerVar = 'I am from outer function'

  function inner() {
    console.log(outerVar)
  }

  inner() // Output: I am from outer function
}

outer()

In this example, the inner function can access the outerVar variable because of lexical scoping. The inner function is lexically within the scope of the outer function, so it has access to the variables declared in outer.

function outer() {
  let outerVar = 'I am from outer function'

  function inner() {
    console.log(outerVar)
  }

  return inner
}

const innerFunction = outer()
innerFunction() // Output: I am from outer function

Closures occur when a function is able to remember and access its lexical scope even when it is executed outside of that scope. In this example, even though the inner function is executed outside of the outer function, it still has access to the outerVar variable due to closure. The inner function closes over the lexical environment of its outer function, allowing it to access variables from that outer function even after the outer function has finished executing.

function counter() {
  let count = 0

  return function () {
    return ++count
  }
}

const increment = counter()

console.log(increment()) // Output: 1
console.log(increment()) // Output: 2
console.log(increment()) // Output: 3

Closures can also be used to create private variables or to maintain state between function calls. In this example, the counter function returns a closure that maintains a private count variable. Each time the returned function is called, it increments and returns the count variable.

function greet(name) {
  setTimeout(function () {
    console.log(`Hello, ${name}!`)
  }, 1000)
}

greet('Rajnish')

Closures are commonly used with callback functions to maintain state or access outer variables. In this example, the anonymous function passed to setTimeout has access to the name variable from the outer greet function due to closure. Even though the greet function has finished executing by the time the callback is invoked, the callback still remembers and can access the name variable.

Privacy in Object-Oriented Programming

In object-oriented programming, scope and closures can be used to create private variables and methods within objects, providing encapsulation and data hiding.

function createPerson(name) {
  let _name = name // private variable

  return {
    getName: function () {
      return _name // closure maintains access to _name
    },
    setName: function (newName) {
      _name = newName // can only be modified through setName method
    },
  }
}

const person = createPerson('Rajnish')
console.log(person.getName()) // Output: Rajnish
person.setName('Kumar')
console.log(person.getName()) // Output: Kumar
console.log(person._name) // Output: undefined (private variable)

In this example, the _name variable is private to the createPerson function and can only be accessed or modified through the returned object's methods (getName and setName). Closures maintain access to the _name variable even after the createPerson function has finished executing, providing encapsulation and data privacy.

Event Handlers and Asynchronous Operations

Scope and closures are commonly used in event handlers and asynchronous operations to maintain access to outer variables within callback functions.

function createCounter() {
  let count = 0

  setInterval(function () {
    count++
    console.log(`Count: ${count}`)
  }, 1000)
}

createCounter()

In this example, the createCounter function creates a closure around the count variable, allowing the setInterval callback function to access and increment the count variable every second. The closure ensures that the count variable is not accessible from outside the createCounter function, maintaining data privacy.

Memoization in Recursive Functions

Closures can be used to implement memoization, a technique used to improve the performance of recursive functions by caching the results of previous function calls.

function fibonacci() {
  let cache = {}

  return function fib(n) {
    if (n in cache) {
      return cache[n]
    } else {
      if (n <= 1) {
        return n
      } else {
        cache[n] = fib(n - 1) + fib(n - 2)
        return cache[n]
      }
    }
  }
}

const memoizedFibonacci = fibonacci()
console.log(memoizedFibonacci(10)) // Output: 55

In this example, the fibonacci function returns a closure that implements memoization for the Fibonacci sequence calculation. The cache object stores the results of previous function calls, allowing the function to return cached values instead of recalculating them, improving performance.

Callback Functions

Callback functions are functions that are passed as arguments to other functions and are executed at a later time or under certain conditions.

function greet(name, callback) {
  console.log(`Hello, ${name}!`)
  callback()
}

function sayGoodbye() {
  console.log('Goodbye!')
}

greet('Rajnish', sayGoodbye)
// Output:
// Hello, Rajnish!
// Goodbye!

In this example, sayGoodbye is a callback function passed to the greet function and executed after the greeting message.

Higher-Order Functions

Higher-order functions are functions that take other functions as arguments or return functions as their result.

function multiplier(factor) {
  return function (x) {
    return x * factor
  }
}

const double = multiplier(2)
console.log(double(5)) // Output: 10

In this example, multiplier is a higher-order function that returns a new function (function(x) { return x * factor; }). This returned function can then be stored in a variable (double) and called later.