If You Drink Water You'll Understand Functions in JavaScript...

Sartaj SinghSartaj Singh
8 min read

Think about a basic necessity—water. Everyone drinks it, and we all need it throughout the day. Now, let’s use this simple concept to understand how functions in JavaScript work.

Imagine you’re working at your desk, deeply focused on an important task. After a while, you feel thirsty. The only way to get water is to walk to the kitchen, grab a glass, fill it from the tap, drink the water, place the glass in the dishwasher, and return to your desk.

A few hours later, the cycle repeats. Every time you need water, you have to go through the same series of repetitive steps—walk to the kitchen, grab a glass, fill it, drink, and return. This process is time-consuming, inefficient, and disrupts your workflow.

Now, instead of manually fetching water each time, you decide to optimize your hydration process. You grab a one-liter water bottle, fill it up, and place it at your desk. Now, whenever you feel thirsty, you just open the bottle and drink—eliminating all unnecessary steps.

This is exactly how functions work in JavaScript.

Initially, we were repeating the same sequence of operations manually—just like writing the same logic multiple times in a program. This redundancy is inefficient. Instead, we create a function (the water bottle), which encapsulates the logic of fetching water. Whenever we need a drink, we just call the function (use the water bottle), executing the predefined steps with minimal effort.

Breaking It Down Technically

  • The kitchen sink represents the core logic or action (fetching water, just like executing some operation in a program).

  • The glass of water represents the return value of a function (what we ultimately need).

  • The entire sequence of going to the kitchen and getting water is a series of imperative instructions that must be executed in order.

  • The water bottle is a function, which encapsulates this entire process into a reusable abstraction.

  • Calling the function (drinking from the water bottle) gives us direct access to the result (water) without repeating all the steps.

This analogy highlights how functions allow us to write cleaner, more maintainable programs by abstracting repetitive logic into reusable components. Instead of manually re-executing the same sequence of steps, we simply call the function, reducing complexity and improving productivity—just like keeping a water bottle at your desk.

Hereby, to re-emphasize a function in JavaScript is like a water bottle—a reusable container that stores a process so you don’t have to repeat it every time. Instead of manually fetching water (executing a series of repetitive steps), you encapsulate the process within a function. Whenever you need water, you simply call the function (use the bottle) and get the result instantly. Functions improve efficiency, improve code reusability, reduce redundancy, and keep code organized, just like how a water bottle makes hydration more convenient.

Let’s discuss different ways of defining a function:

Function Declaration

A function declaration (also called a function statement) is the most common way to define a function in JavaScript. It defines a named function using the function keyword, followed by a name, a set of parameters, and a body enclosed in {}.

function getWater() {
    console.log("Filling a glass of water...");
    return "Glass of water";
}
  • function – The keyword that defines a function.

  • getWater – The function name (used to call the function).

  • () – Parentheses that may contain parameters.

  • {} – Curly braces that contain the function body.

  • return – Specifies the output of the function.

Once defined, a function must be called (invoked) to execute:

let myDrink = getWater(); // Output: Filling a glass of water...
console.log(myDrink);      // Output: Glass of water

Here, getWater() is called, which means the function starts executing. The console.log("Filling a glass of water...") statement runs immediately and prints "Filling a glass of water..." to the console. The function returns "Glass of water", which is then stored in the variable myDrink. Then, the returned value is printed whenever we print the variable myDrink.

Visualizing Function

Let’s say we want to execute the following function drinkWater.

function drinkWater(){    
    return "Glass of water"
}                                 // Declaration

const oneGlass = drinkWater();    // Call

console.log(oneGlass);

Key characteristics of function declaration

  • Hoisted

    Function declarations are hoisted, meaning they are moved to the top of their scope during execution. You can call a function before it is defined, and JavaScript will still recognize it.

sayHello(); // Works, even though the function is declared later

function sayHello() {
    console.log("Hello, world!");
}
// Output: "Hello, world!"
  • Reusability

    A function encapsulates logic, allowing it to be reused multiple times without rewriting code. Example: Instead of manually printing a message multiple times, we use a function.

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

greetUser("SARTAJ");
greetUser("JULES");  // Function is reused

/* Output: 
Hello, SARTAJ!
Hello, JULES!
*/
  • Parameters and Arguments

    Functions can take parameters (placeholders for input values). Arguments are the actual values passed when calling the function.

function pourDrink(drinkType) {
    return `Here is your ${drinkType}`;
}

console.log(pourDrink("coffee")); // Output: Here is your coffee

drinkType is the parameter (defined in the function signature). coffee is the argument (passed when calling the function).

  • Return values

    A function returns a value using the return statement. If there is no return, the function implicitly returns undefined.

      function add(a, b) {
          console.log(a + b);
      }
    
      console.log(add(3, 5)); 
    
      /* Output:
      8
      undefined
      */
    

    When the function add(3, 5) is called, it prints 8 inside the function using console.log(a + b), but since there is no return statement, the function implicitly returns undefined. The outer console.log(add(3, 5)) then prints this undefined to the console. This results in two outputs: 8 from the function and undefined from the outer console.log().

  • Scope and Access

    Functions create their own scope. Variables defined inside a function cannot be accessed outside of it.

function privateFunction() {
    let secret = "Hidden message";
}

console.log(secret); // Error: secret is not defined

Function Expression

A function expression is a way of defining a function in JavaScript by assigning it to a variable. Unlike function declarations, function expressions are not hoisted, meaning they are only available after the line where they are defined.

// Function Expression
const getWater = function() {
    console.log("Filling a cup of water...");
    return "Cup of water";
};

console.log(getWater()); 
/* Output: 
Filling a cup of water...
Cup of water
*/

Here, getWater is a variable that holds the function.

Key characteristics of function expression

  • Not Hoisted

    Unlike function declarations, function expressions are not moved (hoisted) to the top of the script. If you call a function expression before its definition, you get an error.

  •     console.log(drinkWater()); // Error: Cannot access 'drinkWater' before initialization
    
        const drinkWater = function() {
            return "Drinking water...";
        };
    
  • Anonymous function (No name)

    Function expressions can be anonymous, meaning they don’t have a name, and are instead assigned to a variable. This is different from function declarations, which always require a name.

const drink = function() {
    return "Sipping water from the bottle.";
};
  • Used in Callbacks & Higher-Order Functions

    Function expressions are often used when we pass functions as arguments (callback functions).

      const numbers = [1, 2, 3, 4];
    
      numbers.forEach(function (num) {
          console.log(num * 2);
      });
      /* Output:
      2
      4
      6
      8
      */
    

    The forEach method iterates over each element in the numbers array (to learn more about array methods in detail refer to my article- https://shorturl.at/LOj7K). The anonymous function takes each number (num), multiplies it by 2, and logs the result. Here, the function doesn't need a name because it's used only once.

  • More flexible (Can Be Assigned or Reassigned)

    A function expression can be stored in a variable, passed around, or even reassigned.

let waterSource = function() {
    return "Fetching from the tap...";
};

console.log(waterSource()); // Output: Fetching from the tap...

// Reassigning the function
waterSource = function() {
    return "Fetching from the water cooler...";
};

console.log(waterSource()); // Output: Fetching from the water cooler...

Arrow Functions

An arrow function is a more concise way to write a function expression in JavaScript. It uses the => (arrow) syntax and has implicit return behavior, meaning it can return values without needing the return keyword in simple cases. To understand it’s syntax, let’s look at the syntax of a standard function expression:

const getWater = function() {
    return "Glass of water";
};

Using an arrow function, we can write this in a much shorter way:

const getWater = () => "Glass of water";

Notice how the function keyword is removed. Parentheses () for parameters are optional if there's only one parameter. The return keyword is optional for single-line expressions.

Key Characteristics of Arrow Functions

  • Shorter Syntax (Concise & Readable)

    Arrow functions reduce boilerplate code, making functions shorter and cleaner:

const drinkWater = (name) => `Here is your water, ${name}!`;
console.log(drinkWater("SARTAJ")); // Output: Here is your water, SARTAJ!

As opposed to longer function expression:

const drinkWater = function(name) {
    return `Here is your water, ${name}!`;
};
  • Implicit Return

    If the function has a single expression, the return statement is implicitly added.

const fillBottle = () => "Bottle is filled!";
console.log(fillBottle()); // Output: Bottle is filled!

However, for multiple statements, you must use curly braces {} and an explicit return:

const fillBottle = () => {
    console.log("Opening the tap...");
    return "Bottle is filled!";
};
console.log(fillBottle());
/* Output:
Opening the tap...
Bottle is filled!
*/
  • No Own this Context (Lexical this)

    Unlike regular functions, arrow functions do not create their own this. Instead, they inherit this from the surrounding scope, so they work inside methods.

      const person = {
          name: "Sartaj",
          sayHello: function() {
              const innerFunction = () => {
                  console.log(`Hello, ${this.name}`);
              };
              innerFunction();
          }
      };
    
      person.sayHello(); // ✅ Output: "Hello, Sartaj"
    

    The arrow function inherits this from sayHello, so it correctly refers to person.

  • Cannot Be Used as a Constructor

    Arrow functions cannot be used with new because they don’t have their own this.

const WaterBottle = () => {
    this.size = "1L";
};
let myBottle = new WaterBottle(); // Error: WaterBottle is not a constructor

For constructors, use regular function expressions instead.

0
Subscribe to my newsletter

Read articles from Sartaj Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sartaj Singh
Sartaj Singh