Functions in JavaScript

by Gautham Pai

In this article, we will dive into the basics of functions in JavaScript. Functions are one of the fundamental building blocks in programming, allowing us to encapsulate code for reuse and organization. Whether you're just starting out or looking to brush up on your skills, this guide will walk you through what functions are and how to use them effectively.

Getting Started with the Node REPL

Before we begin exploring functions, let's get hands-on by entering the Node.js Read-Eval-Print Loop (REPL). The REPL is an interactive environment that lets you write and test JavaScript code right in your terminal.

To enter the Node REPL, open your terminal and type:

node

You should see a prompt like > indicating that you're now in the Node REPL.

1. Introduction to Functions in JavaScript

The Basic Syntax of a JavaScript Function

Let's start by defining a simple function that adds two numbers together:

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

// Function call
console.log(sum(3, 4)); // Output: 7

In this example:

  • Function Definition: We define a function named sum that takes two parameters, a and b.
  • Return Statement: The function returns the result of a + b.
  • Function Call: We call the function with the arguments 3 and 4 and log the result to the console.

What Happens with Different Argument Types?

JavaScript functions don't enforce types on their input arguments. This means you can pass any type of data to a function, and JavaScript won't complain. However, this flexibility comes with responsibility on the developers to ensure that the right type of arguments are passed to the function.

Let's see what happens if we pass strings instead of numbers:

// Function call with strings
console.log(sum('3', '4')); // Output: '34'
console.log(sum('a', 'b')); // Output: 'ab'

Here, instead of adding numbers, the + operator concatenates the strings:

  • '3' + '4' results in '34'.
  • 'a' + 'b' results in 'ab'.

Note: JavaScript doesn't prevent you from passing the "wrong" types to functions. It's up to you to ensure that the correct types are passed, or to include type checking within your functions.

Functions Always Return a Value

In JavaScript, every function returns a value. If you don't explicitly use a return statement, the function returns undefined by default. This means you can always assign the result of a function to a variable without causing an error.

Let's look at two examples:

console.log("Function that does not seem to return a value (implicitly returns undefined)");

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

var result = printHelloWorld();
console.log(result); // Output: undefined

In this case:

  • printHelloWorld Function: This function logs "Hello World" to the console but doesn't have a return statement.
  • Result Variable: When we assign the function call to result, it holds the value undefined because the function didn't explicitly return anything.
  • Console Output:
    • First, it prints "Hello World".
    • Then, it prints undefined.

Understanding Function Syntax

Let's recap the syntax of defining and using functions:

  • Function Definition:

    function functionName(parameters) {
        // Function body
        return value; // Optional
    }
    
  • Function Call:

    functionName(arguments);
    
  • Example with Return Value:

    function multiply(a, b) {
        return a * b;
    }
    
    console.log(multiply(5, 6)); // Output: 30
    
  • Example without Explicit Return Value:

    function greet(name) {
        console.log("Hello, " + name + "!");
    }
    
    var greeting = greet("Alice");
    console.log(greeting); // Output: undefined
    

2. Calling Functions with More or Fewer Arguments Than Expected

Let us now look at what happens when you call a function with more or fewer arguments than it expects. This is a common scenario in JavaScript, and understanding it can help you write more robust and error-free code.

Calling a Function with More Arguments

Consider the following function definition:

function sum(a, b) {
    return a + b;
}

This sum function is designed to take two parameters, a and b, and return their sum. But what happens if we pass more arguments than it expects?

console.log(sum(3, 4, 5)); // Output: 7

What happened here?

  • Arguments Provided: We passed three arguments: 3, 4, and 5.
  • Parameters Expected: The function expects two parameters: a and b.
  • Result: The function takes the first two arguments (3 and 4) and ignores the extra one (5).
  • Output: The sum of 3 and 4 is 7, which is what gets printed.

Key Takeaway: If you pass more arguments than a function expects, the extra arguments are silently ignored.

💡 Note that it is possible to read all arguments passed to a function with the arguments variable.

Calling a Function with Fewer Arguments

Now, let's see what happens when we call the same function with fewer arguments than it expects.

console.log(sum(3)); // Output: NaN

What happened here?

  • Arguments Provided: We passed only one argument: 3.
  • Parameters Expected: The function expects two parameters: a and b.
  • Parameter Values:
    • a is assigned the value 3.
    • b is assigned the value undefined because we didn't provide a second argument.
  • Result: The expression 3 + undefined is evaluated.
  • Output: Since adding a number and undefined results in NaN (Not-a-Number), the output is NaN.

Key Takeaway: If you pass fewer arguments than a function expects, the missing parameters are assigned undefined.

Using undefined in arithmetic operations results in NaN.

  • When you perform arithmetic operations involving undefined, the result is NaN.
  • This is because undefined is not a number, and JavaScript cannot compute the sum of a number and undefined.

Ignored Extra Arguments

  • Use Case: You can design functions to accept optional parameters, knowing that extra arguments won't cause errors.
  • Example: A logging function that accepts any number of arguments.

Using Default Parameters

Introduced in ES6, default parameters allow you to specify default values for function parameters.

function sum(a, b = 0) {
    return a + b;
}

console.log(sum(3)); // Output: 3
  • Explanation:
    • If b is undefined, it defaults to 0.
    • sum(3) now effectively becomes 3 + 0, which equals 3.

Rewriting the Original Examples with Default Parameters

Let's revisit our earlier examples using default parameters.

function sum(a, b = 0) {
    return a + b;
}

console.log(sum(3, 4, 5)); // Output: 7
console.log(sum(3));       // Output: 3
  • First Call (sum(3, 4, 5)):
    • a = 3, b = 4 (extra argument 5 is ignored).
    • Result: 3 + 4 = 7.
  • Second Call (sum(3)):
    • a = 3, b = 0 (default value).
    • Result: 3 + 0 = 3.

3. Defining Multiple Functions with the Same Name in JavaScript: Overloading or Overwriting?

Let us now explore what happens when you define two functions with the same name. Is it overloading like in other programming languages, or is it something else? Let's find out!

The Scenario: Two Functions with the Same Name

Let's start by entering the following code into the Node.js REPL:

function sum(x, y) {
    return x + y;
}

function sum(x, y, z) {
    return x + y + z;
}

console.log(sum(3, 4));          // Output?
console.log(sum(3, 4, 5, 6));    // Output?

At first glance, it might seem like we're overloading the sum function to handle different numbers of arguments. In some languages like Java or C++, function overloading allows multiple functions with the same name but different parameters. However, JavaScript handles this differently.

What Actually Happens?

When you run the above code, you'll get the following outputs:

NaN
12

Wait, what? Let's break this down step by step.

Function Definitions: Overwriting, Not Overloading

In JavaScript, when you define multiple functions with the same name, the last function definition overwrites the previous ones. So in our code:

  • The first sum function is defined. When we define this function, a variable sum is created and this variable points to a function object.

    function sum(x, y) {
        return x + y;
    }
    
  • When we define a second sum function, it overwrites the first one. This is because the same sum variable that points to the new function object. Note that a variable can only point to one value at a time.

    function sum(x, y, z) {
        return x + y + z;
    }
    

Key Point: JavaScript does not support function overloading in the traditional sense. Instead, the last function with a given name overwrites any previous definitions.

Calling sum(3, 4)

Let's see what happens when we call sum(3, 4) after redefining sum:

console.log(sum(3, 4)); // Output: NaN

sum refers to the second sum function, which expects three parameters (x, y, z). We provided only two arguments: 3 and 4.

Therefore, x = 3, y = 4 and z = undefined (since we didn't provide a third argument).

Thus, x + y + z translates to 3 + 4 + undefined. Adding undefined to a number results in NaN (Not-a-Number) as discussed earlier.

Calling sum(3, 4, 5, 6)

Now let's examine the second function call:

console.log(sum(3, 4, 5, 6)); // Output: 12

This time we provided four arguments: 3, 4, 5, and 6. x = 3, y = 4, z = 5. The extra argument 6 is ignored.

3 + 4 + 5 equals 12.

Best Practices for Handling Multiple Function Signatures

While JavaScript doesn't support traditional function overloading, there are ways to mimic this behavior.

What are the conditions for function overloading?

  1. 2 or more functions have same name but different number of arguments.
  2. 2 or more functions have same name but different type of arguments.
  3. A combination of (1) and (2).

JavaScript does not care about the type of the arguments. So condition (2) is taken care of.

Let us see how we can achieve condition (1) this with a single function in JavaScript.

Using Default Parameters

You can assign default values to parameters, which can help when some arguments are optional.

function sum(x, y, z = 0) {
    return x + y + z;
}

console.log(sum(3, 4));          // Output: 7
console.log(sum(3, 4, 5));       // Output: 12
console.log(sum(3, 4, 5, 6));    // Output: 12 (extra argument ignored)

If z is not provided, it defaults to 0. This way, sum(3, 4) works without resulting in NaN.

Using the arguments Object

The arguments object is an array-like object accessible inside functions that contains the values of the arguments passed to that function.

function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}

console.log(sum(3, 4));          // Output: 7
console.log(sum(3, 4, 5));       // Output: 12
console.log(sum(3, 4, 5, 6));    // Output: 18
  • Explanation:
    • The function doesn't define explicit parameters.
    • It uses arguments.length to determine how many arguments were passed.
    • It sums up all provided arguments, regardless of how many.

Using Rest Parameters

Rest parameters allow you to represent an indefinite number of arguments as an array.

function sum(...numbers) {
    let total = 0;
    for (let i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

console.log(sum(3, 4));          // Output: 7
console.log(sum(3, 4, 5));       // Output: 12
console.log(sum(3, 4, 5, 6));    // Output: 18

Here, the ...numbers syntax gathers all arguments into an array called numbers.

Tips for Avoiding Confusion

  • Use Unique Function Names: To prevent accidental overwriting, ensure your function names are unique unless intentionally redefining.
  • Use Default Parameters or Rest Parameters: These modern JavaScript features provide flexibility in handling variable numbers of arguments.

4. Understanding Variable Scope in JavaScript: var, let, and const

Let us now dive into an essential concept in JavaScript programming: variable scope. We'll explore the differences between var, let, and const, and how they affect the visibility and lifetime of variables in your programs.

Introduction to Variable Scope

In JavaScript, the scope of a variable determines where in your code that variable is accessible. Historically, JavaScript had three types of scope:

  • Global Scope: Variables declared outside of any function or block are in the global scope and can be accessed from anywhere.
  • Function Scope: Variables declared within a function are only accessible inside that function.
  • Closure Scope: (don't worry about this for now).

Before ES6 (ECMAScript 2015), JavaScript did not have block scope, which sometimes led to unexpected behaviors. With the introduction of let and const, JavaScript now supports block scope, allowing for better variable management.

The Traditional var Keyword and Function Scope

Function Scope with var

Variables declared with var inside a function are confined to that function. They are not accessible outside of it.

Example: Function Scope with var

function myFunc() {
    var abc = "Test";
}

myFunc();
console.log('Trying to use abc will throw an error:');
console.log(abc); // ReferenceError: abc is not defined
  • Explanation: The variable abc is declared inside myFunc using var. When we try to access abc outside of myFunc, we get a ReferenceError because abc is not defined in the global scope.

Lack of Block Scope with var

Variables declared with var inside a block (e.g., inside loops or conditional statements) are not confined to that block. They are hoisted to the function or global scope.

Example: No Block Scope with var

for (var i = 0; i < 10; i++) {
    var tmp = i;
}

console.log('i and tmp are still visible:');
console.log(i);   // Output: 10
console.log(tmp); // Output: 9
  • Explanation:
    • The variables i and tmp are declared inside the for loop using var.
    • After the loop, both i and tmp are still accessible and hold values 10 and 9, respectively.
    • This happens because var does not support block scope, so i and tmp become part of the surrounding scope.

Introducing let and Block Scope

ES6 introduced the let keyword, which allows variables to be scoped to the nearest enclosing block.

Block Scope with let

Variables declared with let are confined to the block in which they are defined.

Example: Block Scope with let

function foo() {
    for (var i = 0; i < 10; i++) {
        var x = i;
        let y = i;
    }
    console.log(x); // Output: 9
    console.log(y); // ReferenceError: y is not defined
}

foo();
  • Explanation:
    • x is declared with var and is accessible outside the for loop but within the function foo.
    • y is declared with let and is only accessible inside the for loop.
    • Attempting to access y outside the loop results in a ReferenceError.

Introducing const for Constants

The const keyword, also introduced in ES6, allows you to declare variables whose values are intended to remain constant.

Characteristics of const

  • Block Scoped: Similar to let, const variables are block scoped.
  • Immutable Reference: The variable cannot be reassigned to a new value.

Example: Using const

const a = 10;
a = 20; // TypeError: Assignment to constant variable.
  • Explanation:
    • We declare a as a constant with the value 10.
    • Attempting to reassign a to 20 results in a TypeError.

Note on Mutable Objects with const

While you cannot reassign a const variable, if it's an object or array, you can still modify its properties or elements.

Example: Modifying Objects Declared with const

const obj = { name: 'Alice' };
obj.name = 'Bob'; // This is allowed

console.log(obj.name); // Output: 'Bob'
  • Explanation:
    • The reference obj cannot point to a different object, but the contents of the object can be modified.

Comparing var, let, and const

Featurevarletconst
ScopeFunctionBlockBlock
HoistingYesTemporal Dead Zone (TDZ)TDZ
ReassignmentAllowedAllowedNot Allowed
RedeclarationAllowedNot AllowedNot Allowed

Hoisting and the Temporal Dead Zone

  • Hoisting: JavaScript moves variable declarations to the top of their scope before code execution.
  • Temporal Dead Zone (TDZ): For let and const, variables are hoisted but not initialized, resulting in a TDZ where the variables cannot be accessed until their declaration is evaluated.

Example: Hoisting with var vs. let

console.log(a); // Output: undefined
var a = 5;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
  • Explanation:
    • var a is hoisted and initialized with undefined, so accessing a before its declaration doesn't throw an error.
    • let b is hoisted but not initialized, so accessing b before its declaration results in a ReferenceError.

Best Practices

  • Use let for Variables That Change: When you have variables that will be reassigned, use let.
  • Use const for Constants: For values that should not change, use const to prevent reassignment.
  • Avoid Using var: To prevent unexpected behaviors due to hoisting and lack of block scope, it's generally recommended to avoid using var.

Practical Examples

Loop Variables with let

Using let in loops confines the loop variable to the loop block.

Example: Looping with let

for (let i = 0; i < 5; i++) {
    console.log(i); // Outputs 0 to 4
}

console.log(i); // ReferenceError: i is not defined
  • Explanation:
    • i is only accessible within the loop.
    • Trying to access i outside the loop results in a ReferenceError.

Constants in Functions

Using const inside functions can help ensure certain values remain unchanged.

Example: Constants in a Function

function calculateArea(radius) {
    const pi = 3.14159;
    return pi * radius * radius;
}

console.log(calculateArea(5)); // Output: 78.53975
  • Explanation:
    • pi is declared as a constant because its value shouldn't change.
    • This prevents accidental reassignment within the function.

Summary

Understanding the scope and behavior of variables declared with var, let, and const is essential for writing clean and bug-free JavaScript code.

  • var:

    • Function scoped.
    • Hoisted and initialized with undefined.
    • Can lead to bugs due to lack of block scope.
  • let:

    • Block scoped.
    • Hoisted but not initialized (TDZ applies).
    • Ideal for variables that will be reassigned within a specific block.
  • const:

    • Block scoped.
    • Hoisted but not initialized (TDZ applies).
    • Best for variables that should not be reassigned.

5. Variable Hoisting and Function Hoisting

What is Hoisting?

Hoisting is a JavaScript mechanism where variable and function declarations are moved to the top of their containing scope during the compilation phase. This means that you can use functions and variables before you've actually declared them in your code. However, hoisting behaves differently for variables declared with var, let, and const, as well as for function declarations versus function expressions.

Variable Hoisting with var

Let's start by looking at how variables declared with var are hoisted.

Example: Variable Hoisting with var

function foo() {
    console.log(i, j, k); // Outputs: undefined undefined undefined
    for (var i = 0; i < 10; i++) {
        var j = i;
        if (i % 2 === 0) {
            var k = i;
        }
        if (false) {
            var i = 10,
                j = 20,
                k = 30;
        }
    }
    console.log(i, j, k); // Outputs: 10 9 8
}
foo();

Explanation:

  1. Initial console.log(i, j, k);:

    • At this point, i, j, and k are declared but not yet assigned any values within the function foo. Due to hoisting, their declarations are moved to the top, but their assignments remain in place.
    • Therefore, i, j, and k are all undefined when logged.
  2. Inside the for Loop:

    • The for loop runs from i = 0 to i < 10.
    • Inside the loop:
      • j is assigned the value of i.
      • If i is even, k is assigned the value of i.
      • The if (false) block never executes, so the assignments inside it are ignored.
  3. Final console.log(i, j, k);:

    • After the loop, i has been incremented to 10.
    • j holds the last value assigned inside the loop, which is 9.
    • k holds the last even value assigned, which is 8.

Key Takeaways:

  • Variables declared with var are function-scoped and hoisted to the top of their containing function.
  • Even if you declare a variable inside a block (like a loop or an if statement), var does not respect block scope and makes the variable accessible throughout the function.

Function Hoisting

Function hoisting allows you to call functions before they are defined in your code. This only applies to function declarations, not function expressions.

Example: Function Hoisting

foo(); // Outputs: 'Foo!'

function foo() {
    console.log('Foo!');
}

Explanation:

  • Even though the function foo is defined after its invocation, JavaScript hoists the entire function declaration to the top. This allows you to call foo() before its actual definition in the code.

Contrast with Function Expressions:

bar(); // TypeError: bar is not a function

var bar = function() {
    console.log('Bar!');
};

Explanation:

  • Here, bar is a variable assigned to a function expression.
  • Only the variable declaration (var bar;) is hoisted, not the assignment.
  • Therefore, calling bar() before the assignment results in a TypeError because bar is undefined at that point.

Key Takeaways:

  • Function Declarations are fully hoisted, allowing you to call them before their definition.
  • Function Expressions are not hoisted in the same way; only the variable declaration is hoisted, not the assignment.

6. Default, Spread, and Rest Parameters

JavaScript offers several ways to handle function parameters more flexibly.

Default Parameters

Default parameters allow you to set default values for function parameters. If a parameter is not provided or is undefined when the function is called, the default value is used.

Example: Using Default Parameters

function sum(x, y = 0) {
    return x + y;
}

console.log(sum(1));    // Output: 1
console.log(sum(1, 2)); // Output: 3

Explanation:

  • In the sum function, y has a default value of 0.
  • When sum(1) is called, y is not provided, so it defaults to 0, resulting in 1 + 0 = 1.
  • When sum(1, 2) is called, both x and y are provided, so the function returns 1 + 2 = 3.

Benefits:

  • Simplifies function calls by eliminating the need to pass unnecessary arguments.
  • Makes functions more flexible and easier to use.

Rest Parameters

Rest parameters allow a function to accept an indefinite number of arguments as an array. This is especially useful when you don't know how many arguments will be passed to the function.

Example: Using Rest Parameters

function sum(...numbers) {
    let total = 0;
    numbers.forEach((num) => {
        total += num;
    });
    console.log(total);
}

sum();                // Output: 0
sum(1, 2);            // Output: 3
sum(1, 2, 3);         // Output: 6
sum(1, 2, 3, 4, 5);   // Output: 15

Explanation:

  • The ...numbers syntax collects all arguments passed to the function into an array named numbers.
  • The function then iterates over this array, summing up each number.
  • This allows sum to handle any number of arguments, including none.

Benefits:

  • Provides greater flexibility in function definitions.
  • Simplifies the handling of multiple arguments without explicitly defining each one.

Spread Syntax

Spread syntax allows an iterable (like an array) to be expanded in places where zero or more arguments are expected. It’s the opposite of rest parameters.

Example: Using Spread Syntax

function sum(...numbers) {
    let total = 0;
    numbers.forEach((num) => {
        total += num;
    });
    console.log(total);
}

const nums = [1, 2, 3];
sum(...nums); // Output: 6

Explanation:

  • The ...nums syntax spreads the array nums into individual arguments.
  • This means sum(...nums) is equivalent to sum(1, 2, 3).
  • The function then sums these numbers as before.

Benefits:

  • Makes it easy to pass array elements as separate arguments to functions.
  • Enhances readability and reduces the need for additional code to handle arrays.

Combining Default, Rest, and Spread

These features can be combined to create highly flexible and powerful functions.

Example: Combining Default Parameters and Rest Parameters

function multiply(a, b = 1, ...others) {
    let product = a * b;
    others.forEach((num) => {
        product *= num;
    });
    return product;
}

console.log(multiply(2));           // Output: 2 (2 * 1)
console.log(multiply(2, 3));        // Output: 6 (2 * 3)
console.log(multiply(2, 3, 4));     // Output: 24 (2 * 3 * 4)
console.log(multiply(2, 3, 4, 5));  // Output: 120 (2 * 3 * 4 * 5)

Explanation:

  • a is a required parameter.
  • b has a default value of 1.
  • ...others collects any additional arguments into an array.
  • The function multiplies all provided numbers together, using default values and handling extra arguments gracefully.

Benefits:

  • Maximizes flexibility while maintaining clear and concise code.
  • Allows functions to handle a wide range of input scenarios without requiring multiple overloads.

Practical Examples

1. Using Default Parameters in Real-World Functions

Example: Greeting Function with Default Parameters

function greet(name, greeting = 'Hello') {
    console.log(`${greeting}, ${name}!`);
}

greet('Alice');            // Output: Hello, Alice!
greet('Bob', 'Hi');        // Output: Hi, Bob!
greet('Charlie', 'Good morning'); // Output: Good morning, Charlie!

Explanation:

  • The greet function has a default parameter greeting set to 'Hello'.
  • If no greeting is provided, it defaults to 'Hello'.
  • This makes the function versatile and user-friendly.

2. Summing Numbers with Rest Parameters

Example: Flexible Sum Function

function sum(...numbers) {
    let total = 0;
    numbers.forEach((num) => {
        total += num;
    });
    return total;
}

console.log(sum(1, 2, 3));          // Output: 6
console.log(sum(4, 5));             // Output: 9
console.log(sum(10, 20, 30, 40));   // Output: 100

Explanation:

  • The sum function uses rest parameters to accept any number of arguments.
  • It iterates over the numbers array, adding each number to total.
  • This allows you to sum as many numbers as needed without changing the function's structure.

3. Spreading Arrays into Functions

Example: Using Spread to Pass Array Elements

function displayNumbers(a, b, c) {
    console.log(a, b, c);
}

const nums = [1, 2, 3];
displayNumbers(...nums); // Output: 1 2 3

Explanation:

  • The displayNumbers function expects three separate arguments.
  • By using the spread syntax ...nums, we pass the elements of the nums array as individual arguments.
  • This technique is particularly useful when dealing with arrays of unknown length or when you need to pass array elements to functions that require separate parameters.

Exiting the Node REPL

When you're finished experimenting, you can exit the Node REPL by pressing Ctrl + C twice or typing .exit and pressing Enter.

Test Your Knowledge

Tags