Skip to main content

Command Palette

Search for a command to run...

Javascript Functions: A Comprehensive Guide

Updated
14 min readView as Markdown
Javascript Functions: A Comprehensive Guide
G

Hello! I'm a dedicated software developer with a passion for coding and a belief in technology's impact on our world. My programming journey started a few years ago and has reshaped my career and mindset. I love tackling complex problems and creating efficient code. My skills cover various languages and technologies like JavaScript, Angular, ReactJS, NodeJs, and Go Lang. I stay updated on industry trends and enjoy learning new tools. Outside of coding, I cherish small routines that enhance my workday, like sipping tea, which fuels my creativity and concentration. Whether debugging or brainstorming, it helps me focus. When I'm not coding, I engage with fellow developers. I value teamwork and enjoy mentoring newcomers and sharing my knowledge to help them grow. Additionally, I explore the blend of technology and creativity through projects that incorporate art and data visualization. This keeps my perspective fresh and my passion alive. I'm always seeking new challenges, from open-source contributions to hackathons and exploring AI. Software development is more than a job for me—it's a passion that drives continuous learning and innovation.

Last updated: September 2026 — corrected the claim about matching argument counts and the definition of fn.length; added this and its binding rules, call/apply/bind, default and rest parameters, IIFEs, and where arrow functions actually differ.

Welcome to our comprehensive guide on JavaScript Functions. In this article we'll take an in-depth look at JavaScript functions, covering both traditional function syntax and modern arrow functions — including the places where the two are genuinely not interchangeable.

1. Introduction to Functions

Functions are fundamental to JavaScript, serving as the building blocks for structuring your code. They let you encapsulate specific tasks, making your code more organized and manageable. A function is a reusable block of code designed to perform a particular task when invoked.

Let's break down the structure of a JavaScript function:

function functionName(parameters) {
    // Code to be executed
}

Arrow syntax (ES6):

const functionName = (parameters) => {
    // Code to be executed
};

In this structure:

  • functionName: The name of the function, which should describe its purpose.
  • parameters: Optional placeholders for values the function expects when called.
  • Code to be executed: The actual JavaScript enclosed within curly braces {}.

Example:

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

greet("Alice");   // Hello, Alice!

Arrow:

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

greet("Alice");   // Hello, Alice!

A warning before we go further

Throughout this article you will see both syntaxes side by side. In most of these examples they behave identically — but arrow functions are not just shorter function syntax. They differ in five ways that matter, and section 5a covers each one.

If you take only one thing from this article, take that.

2. Defining Functions

Defining a function uses the function keyword, followed by a name and optional parameters. Here is one that calculates the area of a rectangle:

function calculateRectangleArea(length, width) {
    return length * width;
}

const area = calculateRectangleArea(5, 3);
console.log(area);   // 15

Arrow:

const calculateRectangleArea = (length, width) => length * width;

const area = calculateRectangleArea(5, 3);
console.log(area);   // 15

Note the arrow version has no braces and no return. When an arrow's body is a single expression, that expression is returned automatically — an implicit return. Add braces and you must write return yourself:

const area1 = (l, w) => l * w;              // implicit return
const area2 = (l, w) => { return l * w; };  // explicit return
const area3 = (l, w) => { l * w; };         // returns undefined — no return statement

One gotcha: to implicitly return an object literal, wrap it in parentheses, or JavaScript reads the braces as a function body:

const makeUser = (name) => ({ name });   // parentheses required
console.log(makeUser("Ganesh"));         // { name: 'Ganesh' }

3. Invoking Functions

Invoking a function executes its code. Use its name followed by parentheses, optionally passing arguments:

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

const result = addNumbers(5, 3);
console.log(result);   // 8

Arrow:

const addNumbers = (a, b) => a + b;
console.log(addNumbers(5, 3));   // 8

A function with no return, or with a bare return;, gives back undefined.

4. Function Arguments and Parameters

Parameters are the variables listed in a function's definition. Arguments are the actual values passed when it is called.

function greet(name) {          // "name" is a parameter
    return `Hello, ${name}!`;
}

greet("Alice");                 // "Alice" is an argument

You do not have to match the counts

JavaScript does not check that the number of arguments matches the number of parameters. Passing too few or too many is legal and silent:

greet();                    // "Hello, undefined!"  — missing parameter is undefined
greet("A", "B", "C");       // "Hello, A!"          — extra arguments are ignored

No error either way. That is convenient and it is also how a typo goes unnoticed for a week, which is one of the reasons TypeScript exists.

Default parameters

Before ES6, a missing argument meant boilerplate at the top of every function:

function greet(name) {
    name = name || "there";   // and this breaks when someone passes ""
    return `Hi ${name}`;
}

Now the default lives in the signature:

function greet(name = "there") {
    return `Hi ${name}`;
}

greet();            // "Hi there"
greet("Ganesh");    // "Hi Ganesh"
greet("");          // "Hi "        — an empty string is a real value, so no default
greet(undefined);   // "Hi there"   — only undefined triggers it
greet(null);        // "Hi null"    — null does NOT trigger it

That is the difference from the || version. The default fires only for undefined — not for "", 0, false or null.

Defaults are evaluated at call time, and later parameters can use earlier ones:

function makeRange(start = 0, end = start + 10) {
    return [start, end];
}

makeRange();    // [ 0, 10 ]
makeRange(5);   // [ 5, 15 ]

Rest parameters

To accept an unknown number of arguments, use a rest parameter. It collects them into a real array:

function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);      // 6
sum();             // 0
sum(...[4, 5]);    // 9  — spread at the call site

It can follow named parameters, and must come last:

function log(level, ...messages) {
    return `[${level}] ${messages.join(" ")}`;
}

log("INFO", "user", "logged", "in");   // "[INFO] user logged in"

Do not confuse rest with spread. They use the same ... and do opposite things: rest collects several values into an array where a name is being bound; spread expands an array into separate values at a call site or inside a literal.

The older way to do this was the arguments object, which we come back to in section 5a — it has a catch.

5. Functions As Values

JavaScript treats functions as first-class citizens. You can assign them to variables, pass them as arguments, store them in arrays and objects, and return them from other functions.

const sayHello = function () {
    console.log("Hello, world!");
};

sayHello();   // Hello, world!

Arrow:

const sayHello = () => {
    console.log("Hello, world!");
};

Because functions are values, you can build functions that take or return functions — higher-order functions:

// Takes a function
function repeat(times, action) {
    for (let i = 0; i < times; i++) action(i);
}
repeat(3, (i) => console.log(i));   // 0, 1, 2

// Returns a function
const multiply = (a) => (b) => a * b;
const double = multiply(2);
console.log(double(5));       // 10
console.log(multiply(3)(4));  // 12

That second pattern — a function returning a function so arguments can be supplied one at a time — is called currying. It is what lets you build a specialised function (double) out of a general one (multiply).

5a. Where Arrow Functions Actually Differ

The two syntaxes are not interchangeable. Five differences, each of which will eventually bite.

i. Arrow functions have no this of their own

This is the big one. A function gets this from how it is called. An arrow reads this from where it was written, and nothing can change that.

const user = {
    name: "Ganesh",
    regular() { return this.name; },
    arrow: () => this?.name,
};

user.regular();   // "Ganesh"
user.arrow();     // undefined — `this` is not the object

Never write an object method as an arrow. But inside a method, an arrow is exactly what you want:

const timer = {
    seconds: 0,

    startBroken() {
        setInterval(function () {
            this.seconds++;      // `this` is undefined — plain function call
        }, 1000);
    },

    startWorking() {
        setInterval(() => {
            this.seconds++;      // `this` is the timer, inherited from startWorking
        }, 1000);
    },
};

ii. Arrow functions have no arguments

function hasArgs() { return arguments.length; }
hasArgs(1, 2, 3);   // 3

const noArgs = () => arguments.length;
noArgs(1, 2, 3);    // ReferenceError: arguments is not defined

If you write arrows — and most modern code does — rest parameters are your only option.

iii. Arrow functions cannot be constructors

function Person(name) { this.name = name; }
new Person("Ganesh");        // works

const ArrowPerson = (name) => { this.name = name; };
new ArrowPerson("Ganesh");   // TypeError: ArrowPerson is not a constructor

iv. Function declarations are hoisted; arrows are not

hoisted();   // works — function declarations are hoisted whole
function hoisted() { return "fine"; }

notHoisted();   // ReferenceError: Cannot access 'notHoisted' before initialization
const notHoisted = () => "x";

An arrow assigned to const sits in the temporal dead zone until its line runs.

v. Arrows cannot be generators

There is no async-style arrow equivalent of function*.

The practical rule:

  • Arrow for callbacks, and anywhere you want the surrounding this.
  • function for object methods, and for anything called with new.

6. Functions As Namespaces

Functions can serve as namespaces, keeping variables out of the global scope and avoiding naming conflicts:

const mathUtils = () => {
    const pi = 3.14159;

    const calculateArea = (radius) => pi * radius * radius;

    return { calculateArea };
};

const utils = mathUtils();
console.log(utils.calculateArea(5));   // 78.53975
console.log(typeof pi);                // "undefined" — pi never escaped

IIFE — and why you probably do not need one

An Immediately Invoked Function Expression runs the moment it is defined:

(function () {
    const secret = "hidden";
    console.log("runs immediately");
})();

The wrapping parentheses matter. Without them, JavaScript reads function at the start of a line as a declaration, which cannot be invoked on the spot. The parentheses force it to be read as an expression. The arrow version is shorter:

(() => {
    console.log("also runs immediately");
})();

For twenty years this was the standard way to keep variables out of the global scope, because var had no block scoping. Today let and const do that with a bare block:

{
    const secret = "hidden";   // scoped to this block, no function needed
}

And ES modules are scoped by default — nothing leaks unless it is exported. So IIFEs are largely historical. Recognise the pattern in older or bundled code; you rarely need to write one.

7. Closures

Closures let an inner function access the variables of its outer function even after the outer function has finished. This is what makes private state possible.

function counter() {
    let count = 0;

    return function () {
        return ++count;
    };
}

const increment = counter();
console.log(increment());   // 1
console.log(increment());   // 2
console.log(typeof count);  // "undefined" — count is unreachable from outside

Arrow:

const counter = () => {
    let count = 0;
    return () => ++count;
};

Two things worth knowing.

Each call creates a fresh closure. Two counters do not share state:

const a = counter();
const b = counter();
a(); a();      // a is at 2
console.log(b());   // 1 — b has its own count

Closures keep their variables alive. That is the point, and also how memory leaks happen — a closure held by an event listener keeps everything it captured in memory until the listener is removed.

Closures are behind memoisation, the module pattern, and every "run this only once" helper:

function once(fn) {
    let done = false;
    let value;
    return (...args) => {
        if (!done) { done = true; value = fn(...args); }
        return value;
    };
}

const init = once(() => "ran");
console.log(init(), init());   // "ran ran" — but the function ran only once

8. Function Properties and Methods

Functions in JavaScript are objects, so they have properties and methods of their own.

name and length

function example(a, b, c) {}

console.log(example.name);     // "example"
console.log(example.length);   // 3

length is not simply the number of parameters. It counts parameters before the first one with a default value or a rest parameter:

function withDefault(a, b = 2, c) {}
console.log(withDefault.length);   // 1  — stops at the first default

function withRest(a, ...rest) {}
console.log(withRest.length);      // 1  — rest does not count

call, apply and bind

These three set this explicitly, and they are the answer to the "detached method" bug.

Pull a method off its object and it stops working, because this comes from the call:

const user = { name: "Ganesh", greet() { return `Hi, ${this.name}`; } };

user.greet();          // "Hi, Ganesh"

const greet = user.greet;
greet();               // TypeError: Cannot read properties of undefined

Nothing about the function changed — only the call did. There is no object before the dot any more. The same thing happens when you pass a method as a callback:

setTimeout(user.greet, 100);          // broken, same reason
setTimeout(() => user.greet(), 100);  // fine — the call still has the dot

call and apply invoke the function immediately with a this you supply. They differ only in how arguments are passed — call takes them separately, apply takes an array:

function introduce(greeting, punctuation) {
    return `${greeting}, I am ${this.name}${punctuation}`;
}

const ganesh = { name: "Ganesh" };

introduce.call(ganesh, "Hello", "!");      // "Hello, I am Ganesh!"
introduce.apply(ganesh, ["Hi", "..."]);    // "Hi, I am Ganesh..."

bind is different: it does not call the function. It returns a new function with this permanently attached:

const boundGreet = user.greet.bind(user);

boundGreet();                 // "Hi, Ganesh"
setTimeout(boundGreet, 100);  // works — this cannot be lost

A bound function stays bound — calling .bind() on it again will not change this. And none of the three works on an arrow function, which has no this to set.

A way to remember which is which: call takes commas, apply takes an array, bind gives you back a function.

9. Functional Programming

JavaScript supports functional patterns — building programs out of small functions that transform data instead of mutating it.

const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.map((num) => num * 2);
const evens = numbers.filter((num) => num % 2 === 0);
const total = numbers.reduce((sum, num) => sum + num, 0);

console.log(doubled);   // [ 2, 4, 6, 8, 10 ]
console.log(evens);     // [ 2, 4 ]
console.log(total);     // 15
console.log(numbers);   // [ 1, 2, 3, 4, 5 ] — untouched

That last line is the point. map, filter and reduce all return something new and leave the input alone, which is what makes chains predictable:

const result = numbers
    .filter((n) => n % 2 === 1)
    .map((n) => n * 10);

console.log(result);   // [ 10, 30, 50 ]

Pure functions

A function is pure when it depends only on its arguments and changes nothing outside itself. Same input, same output, every time:

// Pure — depends only on its input
const add = (a, b) => a + b;

// Impure — reads outside state, and mutates it
let count = 0;
const increment = () => ++count;

Pure functions are easier to test (no setup), easier to cache, and safe to reorder. Not everything can be pure — something has to write to the DOM or call an API eventually — but pushing side effects to the edges and keeping the middle pure is most of what "functional style" means in practice.

10. Summary

We've covered defining and invoking functions, parameters and arguments, functions as values, closures, function properties, and functional patterns.

Three things worth carrying away:

  • Arrow functions are not shorter function syntax. They have no this, no arguments, cannot be constructors, and are not hoisted. Use arrows for callbacks, function for methods and constructors.
  • this depends on how a function is called, not where it is written. A method pulled off its object loses it — bind is the fix.
  • JavaScript never checks your argument count. Default and rest parameters are how you handle that deliberately.

Next in the series: JavaScript Classes.

Hope you like it, if yes ❤️ like & 📤share.

Thanks for your time.

Happy coding….

← JavaScript Arrays JavaScript Classes →

More from this blog

H

Hacker's Haven

28 posts

I'm a Senior Software Developer with expertise in programming languages and tech. Follow me for detailed articles and tutorials on software development tools and frameworks.