Beyond the Basics: Understanding Advanced JavaScript Functions

10 min read, Mon, 26 May 2025

JavaScript Types Image from pixabay.com

You’ve probably used JavaScript functions a bunch already. They’re those handy blocks of code that do specific tasks, like console.log() or a function you wrote yourself to add two numbers. But under the hood, functions in JavaScript are a lot more interesting and powerful than just a simple do this command. Let’s dive deeper!

What’s Really Going On: Callable Objects

Think of a function in JavaScript not just as a piece of code, but as a special kind of object that you can “call” or “run.” These are called callable objects.

Every function in JavaScript has two hidden abilities, or “internal slots,” that make it special:

Functions essentially encapsulate (package up) specific pieces of JavaScript code. This makes your code reusable and organized.

Here are some standard properties you’ll often see on functions:

  1. length: This property tells you how many arguments the function expects. It’s a non-negative number.
  2. name: This is a string that describes the function, usually its name (e.g., ‘myFunction’).
  3. prototype: Many functions, especially those that can be used with new, have a prototype property. This is an object where you can add properties and methods that will be inherited by objects created from that function.
  4. new.target: Knowing How You Were Called

Inside a function, the special keyword new.target tells you if the function was called as a regular function (e.g., myFunction()) or as a constructor using the new keyword (e.g., new myFunction()). If new.target has a value, it means new was used. This is useful for writing flexible functions.

Below is an example of new.target:

function Foo() {
    if (new.target === undefined) {
        console.log(
            "This is not a constructor like call. It's a regular function call."
        );
    }
}

Foo(); //This is not a constructor like call. It's a regular function call.
new Foo(); // no output

The Function Family: Different Types of JavaScript Functions

Not all functions are created equal! JavaScript has several types, each with its own quirks and uses. To understand them better, let’s talk about “ordinary” versus “exotic” objects.

In JavaScript, most objects you work with are ordinary objects. This means they largely follow a standard, predictable set of rules for how they behave internally—how they store properties, how this works, and so on.

An exotic object, on the other hand, is an object that doesn’t entirely follow these standard rules for all its internal operations. It has some unique, non-standard behavior that makes it “exotic.”

Different Function Types:

Let’s look at the function types

1. Function Objects (The “Ordinary” Ones):

These are the most common functions you’ll write using function declarations or expressions. They are called “ordinary” because they largely stick to the standard internal rules for JavaScript objects.

// An ordinary function declaration
function calculateArea(length, width) {
    return length * width;
}

// You can add properties to it just like any object
calculateArea.purpose = "Utility function";
console.log(calculateArea.purpose); // Utility function

// Its 'this' depends on how it's called
const myObject = {
    name: "MyObject",

    logName: function () {
        console.log(this.name);
    },
};

myObject.logName(); // MyObject

const unboundLog = myObject.logName;

unboundLog(); // undefined; because global this do not have name property.

In the example above, calculateArea is an ordinary function; we can add a property to it like any other object. For myObject.logName, its this context changes based on how it’s invoked.

2. Built-in Functions:

These are functions that come pre-packaged with JavaScript, like parseInt() or Math.exp(). They are also ordinary objects, but they have specific internal slots like [[Realm]] (related to their environment) and [[InitialName]].

3. Bound Function Exotic Objects:

This is a perfect example of an “exotic” function. When you use the bind() method on an ordinary function, it creates a new function called a “bound function.” This new function is “exotic” because its this value is permanently fixed to whatever you bound it to. No matter how you call it (even with call() or apply()), its this context won’t change.

const person = {
    name: "Alice",
    greet: function () {
        console.log(`Hello, my name is ${this.name}`);
    },
};

const anotherPerson = {
    name: "Bob",
};

// Ordinary function call, 'this' changes based on caller
person.greet.call(anotherPerson); // Hello, my name is Bob

// Create a bound function, fixing 'this' to 'person'
const boundGreet = person.greet.bind(person);

// Call the bound function - 'this' is 'person'
boundGreet(); //Hello, my name is Alice

// Try to change 'this' for the bound function using .call()
boundGreet.call(anotherPerson); // Hello, my name is Alice

In this example, person.greet.call(anotherPerson) successfully changes this to anotherPerson because greet is an ordinary function. However, when we create boundGreet using bind(person), its this is permanently set to person. Even calling boundGreet.call(anotherPerson) cannot override this fixed this value, which is the “exotic” behavior of a bound function.

4. Method Functions:

When a function is associated with an object via a property, it’s called a method. For example, myObject.doSomething() where doSomething is a function.


Making Functions: How We Create Them

There are several ways to write and define functions in JavaScript:

Function Declaration:

This is probably the most common way you’ve seen. You use the function keyword, give it a name, and define its code block.

function greet(name) {
    return `Hello, ${name}!`;
}

These are created as regular function objects and usually have a prototype property.

Function Expression:

Similar to a declaration, but the function is part of an expression, often assigned to a variable. It can be named or anonymous (without a name).

const sayHello = function (name) {
    return `Hello, ${name}!`;
};

Arrow Function Definitions:

These are a more concise way to write functions, using the => syntax.

const add = (a, b) => a + b;
Key characteristics:

Method Definitions:

These are functions used directly within object literals or class definitions.

const myObject = {
    value: 10,
    increment() {
        // This is a method definition
        this.value++;
    },
};

class MyClass {
    constructor(name) {
        this.name = name;
    }
    sayName() {
        // This is also a method definition
        console.log(this.name);
    }
}

Method definitions have a [[HomeObject]] internal slot, which is used when you make super calls (for inheritance). Regular methods are not constructors and don’t have a prototype property.

Generator Functions:

Defined using function*. When you call a generator function, it doesn’t run all its code at once. Instead, it returns a special GENERATOR object. You can then “pause” and “resume” the function’s execution using the yield keyword.

function* idMaker() {
    let id = 0;

    while (true) {
        yield id++;
    }
}

Async Functions:

Defined using async function. These functions are designed to work with Promises. When you call an async function, it returns a PROMISE that will eventually resolve with the function’s result or reject if an error occurs. You often use the await keyword inside them to pause execution until a Promise settles.

async function fetchData() {
    const response = await fetch("some-api-url");
    const data = await response.json();
    return data;
}

Async Generator Functions:

These are a combination of async and generator functions, defined using async function\*. When called, they return an ASYNC GENERATOR object. This allows you to await results from yield expressions, combining asynchronous operations with the pause/resume capabilities of generators.

Async Arrow Functions:

These are arrow functions that are also async. Defined using async and =>.

const getData = async () => {
    const response = await fetch("another-api-url");
    return response.json();
};

Like regular arrow functions, they have this set lexically (meaning this is taken from the surrounding code) and no arguments object. They return a Promise.

Dynamic Function Creation:

You can also create functions from strings at runtime using built-in constructors. While powerful, this is generally less common and can be less performant or safe than other methods, so use it carefully. The string provided to the constructor is essentially treated as the function’s body.

For example, you can use the Function constructor:

const myFunctionFromString = new Function("a", "b", "return a + b");
console.log(myFunctionFromString(2, 3));

Here, ‘a’ and ‘b’ are the argument names, and ‘return a + b’ is the function’s code body as a string.

You can also dynamically create Generator, Async, and Async Generator functions:

const GeneratorFunction = Object.getPrototypeOf(function* () {}).constructor;
const genFunc = new GeneratorFunction("a", "yield a * 2");

const generator = genFunc(5);
console.log(generator.next().value);

In this case, GeneratorFunction is obtained by getting the constructor of a normal generator function’s prototype. The arguments to new GeneratorFunction() are ‘a’ (the argument name) and yield a \* 2 (the function body). The yield keyword within the string makes it a generator function.

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

const asyncFunc = new AsyncFunction(
    "a",
    "return await Promise.resolve(a + 10)"
);

asyncFunc(7).then((result) => console.log(result));

Similarly, AsyncFunction is obtained from an async function’s prototype. The function body string return await Promise.resolve(a + 10) includes the await keyword, making it an asynchronous function that returns a Promise.

const AsyncGeneratorFunction = Object.getPrototypeOf(
    async function* () {}
).constructor;

const asyncGenFunc = new AsyncGeneratorFunction(
    "a",
    "yield await Promise.resolve(a * 3)"
);

async function runAsyncGen() {
    const asyncGenerator = asyncGenFunc(4);
    console.log(await asyncGenerator.next());
}

runAsyncGen();

Finally, AsyncGeneratorFunction is obtained from an async generator function’s prototype. The body string yield await Promise.resolve(a \* 3) combines yield for generator behavior and await for asynchronous operations, resulting in an Async Generator.

Another way to create a function with specific this and initial arguments is using the bind method: Function.prototype.bind() creates a new bound function exotic object. This new function has its this value permanently fixed to whatever you pass to bind, and any arguments you provide to bind will be placed at the beginning of the bound function’s arguments.


Built-in Function Methods: call, apply, and bind

JavaScript functions themselves have some useful methods you can call on them:

bind(thisArg, …args):

As mentioned above, bind creates a new, exotic bound function. The this value of this new function will always be thisArg, and any args you pass to bind will be pre-filled as the first arguments of the new function.

call(thisArg, …args):

This method executes a function immediately. The first argument (thisArg) sets the this value for that specific call, and the subsequent arguments (...args) are passed to the function individually.

function greet() {
    console.log(`Hello from ${this.name}`);
}
const person = { name: "Alice" };
greet.call(person);

apply(thisArg, argArray):

Similar to call, apply also executes a function immediately and sets its this value. The key difference is that apply takes its arguments as an array-like object (e.g., an actual array).

function sum(a, b) {
    return a + b;
}
sum.apply(null, [5, 3]);

These methods are incredibly useful for controlling the this context of a function and for passing arguments in different ways.


Conclusion: Mastering the Function Frontier

We’ve covered a lot of ground, haven’t we? From understanding functions as powerful callable objects with their hidden [[Call]] and **[[Constructor]] ** abilities, to distinguishing between everyday ordinary functions and specialized exotic ones like bound functions, you now have a much clearer picture of what’s happening under the hood.

We explored the diverse ways functions come into being—from classic declarations and expressions to modern arrow functions, powerful generators, and asynchronous wonders. And let’s not forget those handy built-in methods like call, apply, and bind that give you incredible control over function execution.

JavaScript functions are truly the workhorses of the language. By grasping these advanced concepts, you’re not just writing code; you’re writing more efficient, flexible, and powerful JavaScript. Keep experimenting, keep building, and you’ll master this fundamental aspect of web development in no time!