Skip to main content

Command Palette

Search for a command to run...

Why Does JavaScript Feel So AbSuRd? Part - 3

A weird lowkey autistic language like me .

Updated
12 min readView as Markdown
Why Does JavaScript Feel So AbSuRd? Part - 3
R
cracked noob dev . 21

let's continue gng !

Hoisting

What is hoisting really?

The beginner explanation usually says that JavaScript "moves declarations to the top". That is a useful first approximation, but technically it is too vague.
A better model is that JavaScript creates the execution environment before executing the code and registers declarations in their appropriate scope.
Different declarations are initialised differently.

console.log(hoistedVar);

var hoistedVar = 'value';
// undefined

The var binding exists and is initialised to undefined before the assignment executes.
With let and const, the binding exists but is not initialised until execution reaches the declaration.

console.log(hoistedLet);

let hoistedLet = 'value';
// ReferenceError

The period between entering the scope and reaching the declaration is called the Temporal Dead Zone, or TDZ.
Function declarations behave differently again.

hoistedFn();

function hoistedFn() {
  console.log('works!');
}

The function declaration is fully initialised before execution reaches the call.
A function expression does not work the same way.

hoistedExpr();

var hoistedExpr = function () {
};

The variable itself is initially undefined, so attempting to call it produces a TypeError.
Classes are also affected by the TDZ.

new MyClass();

class MyClass {
}

Closures

What is a closure?

A closure is a function together with the lexical environment in which that function was created.
That definition sounds academic, but the behaviour is easier to see in code.

function makeCounter() {
  let count = 0;

  return {
    increment() {
      return ++count;
    },

    decrement() {
      return --count;
    },

    get value() {
      return count;
    },
  };
}

const counter = makeCounter();

counter.increment();
// 1

counter.increment();
// 2

counter.value;
// 2

counter.count;
// undefined

How does debounce work?

Debouncing means delaying an operation until activity has stopped for a specified amount of time.
This is extremely common in search inputs. If a user types hello, you usually do not want to send a network request for h, then he, then hel, then hell, and finally hello.

function debounce(fn, delay = 300) {
  let timeoutId;

  return function (...args) {
    clearTimeout(timeoutId);

    timeoutId = setTimeout(
      () => fn.apply(this, args),
      delay
    );
  };
}

const search = debounce(
  query => fetchResults(query),
  400
);

input.addEventListener(
  'input',
  e => search(e.target.value)
);

Why does debounce use fn.apply(this, args)?

apply() invokes a function with a specified this value and an array of arguments.

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

const person = {
  name: 'Rohit'
};

introduce.apply(person, ['Hello', '!']);
// 'Hello, I am Rohit!'

In the debounce implementation, the wrapper function is deliberately a regular function.

return function (...args) {
  clearTimeout(timeoutId);

  timeoutId = setTimeout(
    () => fn.apply(this, args),
    delay
  );
};

What is throttling?

Debouncing waits until activity stops. Throttling limits how frequently something can run.
A throttled function can be called many times, but the actual operation is restricted to a particular interval.

function throttle(fn, limit = 300) {
  let inThrottle = false;
  let lastArgs = null;

  return function (...args) {
    if (inThrottle) {
      lastArgs = args;
      return;
    }

    fn.apply(this, args);
    inThrottle = true;

    setTimeout(() => {
      inThrottle = false;

      if (lastArgs) {
        fn.apply(this, lastArgs);
        lastArgs = null;
      }
    }, limit);
  };
}

How does memoisation use closures?

Memoisation means caching the result of a function so that repeated calls with the same inputs can reuse previously calculated results.

function memoize(fn) {
  const cache = new Map();

  return function (...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    }

    const result = fn.apply(this, args);

    cache.set(key, result);

    return result;
  };
}

What does once() do?

once() creates a function that allows another function to execute only once.

function once(fn) {
  let called = false;
  let result;

  return function (...args) {
    if (called) {
      return result;
    }

    called = true;
    result = fn.apply(this, args);

    return result;
  };
}

const initialize = once(() => {
  console.log('init');
  return { ready: true };
});

initialize();
// logs and returns { ready: true }

initialize();
// no log, returns the cached object

Module Pattern

Can closures create private data?

Yes. The classic module pattern is essentially a closure used to expose a controlled public API while keeping internal state private.

const UserStore = (function () {
  const users = new Map();
  let nextId = 1;

  function validate(user) {
    if (!user.email?.includes('@')) {
      throw new Error('Invalid email');
    }
  }

  return {
    add(user) {
      validate(user);

      const id = nextId++;

      users.set(id, {
        ...user,
        id
      });

      return id;
    },

    get(id) {
      return users.get(id);
    },

    get size() {
      return users.size;
    },
  };
})();

UserStore.add({
  email: 'a@b.com'
});

UserStore.users;
// undefined

UserStore.validate;
// undefined

What are the five main this rules?

  1. new binding When a function is called with new, this refers to the newly created object.
function Person(name) {
  this.name = name;
}

const person = new Person('Rohit');

console.log(person.name);
// 'Rohit'
  1. Explicit binding call(), apply(), and bind() allow you to explicitly control this.
function greet() {
  return `Hi, ${this.name}`;
}

greet.call({
  name: 'Rohit'
});
// 'Hi, Rohit'

greet.apply(
  { name: 'Rohit' },
  []
);
// 'Hi, Rohit'

const bound = greet.bind({
  name: 'Rohit'
});

bound();
// 'Hi, Rohit'

call() and apply() execute immediately. bind() creates a new function that remembers the chosen this.

3. Implicit binding When a function is called as an object method, the object before the dot becomes this.

const obj = {
  name: 'Rohit',

  greet() {
    return `Hi, ${this.name}`;
  }
};

obj.greet();
// 'Hi, Rohit'

The call site is what matters here. The function is being called as obj.greet(), so this is obj.

4. Default binding A plain function call does not provide an object for implicit binding.

function standalone() {
  return this;
}

standalone();

In strict mode and modules, this is undefined. In non-strict code, the default can be globalThis.

5. Arrow functions Arrow functions do not create their own this.

const arrow = () => this;

arrow.call({
  name: 'X'
});

Why does this disappear when I store a method in another variable?

Consider this.

const user = {
  name: 'Rohit',

  greet() {
    return `Hi, ${this.name}`;
  },
};

user.greet();
// 'Hi, Rohit'

const fn = user.greet;

fn();
// 'Hi, undefined'

Why does setTimeout(user.greet, 100) lose this?

Because you are passing the function itself to another piece of code.

setTimeout(user.greet, 100);

This effectively says, "Here is the function. Call it later."
The timer does not remember that you originally obtained the function from user.greet. The relationship between the function and the object is not stored inside the function.
Compare that with this.

setTimeout(() => user.greet(), 100);

How can we preserve this?

One option is bind().

setTimeout(
  user.greet.bind(user),
  100
);

Another option is to use a wrapper arrow function.

setTimeout(
  () => user.greet(),
  100
);

For class instances, a class-field arrow function is another option when you specifically need a method that survives detachment.

class User {
  name = 'Rohit';

  greet = () => `Hi, ${this.name}`;
}

The class-field arrow gets its lexical this from the instance initialization context. The trade-off is that each instance gets its own function rather than sharing one function on the prototype.


What is the difference between call(), apply(), and bind()?

All three are related to controlling this, but they differ in when the function executes and how arguments are supplied.

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

const person = {
  name: 'Rohit'
};

introduce.call(
  person,
  'Hello',
  '!'
);
// 'Hello, I am Rohit!'

introduce.apply(
  person,
  ['Hello', '!']
);
// 'Hello, I am Rohit!'

const bound = introduce.bind(
  person,
  'Hello'
);

bound('!');
// 'Hello, I am Rohit!'

Method Borrowing

What is method borrowing?

JavaScript methods are functions, so sometimes you can borrow a method from one object or prototype and call it with another object as its this.
For example, an array method can operate on an array-like object.

const arrayLike = {
  0: 'a',
  1: 'b',
  length: 2
};

Array.prototype.join.call(
  arrayLike,
  '-'
);
// 'a-b'

The object is not actually an Array, but it has the indexed properties and length that join() needs.
Modern JavaScript often provides cleaner alternatives.

Array.from(arrayLike);
// ['a', 'b']

Still, understanding method borrowing helps explain why call() and apply() are more than just interview trivia.


Putting It All Together

Why are closures, higher-order functions, this, scope, and reduce() all appearing in the same chapter?

What should I actually remember from this entire section?

Build the mental model first.
Functions in JavaScript are values, which is why they can be stored, passed around, and returned. Higher-order functions take advantage of that ability to build reusable behaviour around other functions.
Regular functions and arrow functions are not interchangeable. Regular functions have dynamic this, their own arguments, and can be constructors. Arrow functions use lexical this, do not have their own arguments, and cannot be used with new.
Scope is lexical. JavaScript resolves variables by walking outward through the scope chain from where the code was written.
Hoisting is better understood as environment creation and declaration initialisation than as variables physically moving upward. var is initialised to undefined, while let, const, and classes remain uninitialised in the TDZ until execution reaches their declarations. Function declarations are fully initialised.
Closures happen when a function retains access to its lexical environment. That single mechanism powers private state, callbacks that remember context, debouncing, throttling, memoisation, currying, and the classic module pattern.
reduce() is not just a fancy way to add numbers. It is a general accumulation mechanism, and it becomes particularly interesting when the accumulated value is passed through a sequence of functions.
Composition combines functions from right to left, while pipe() usually expresses the same idea from left to right.
Finally, this for regular functions is determined by how the function is called. When you detach an object method, you detach the implicit receiver too. Arrow functions behave differently because their this is lexical.
Once these ideas click, a lot of JavaScript stops looking like a random collection of language quirks and starts looking like several features built on top of a few consistent mechanisms.
And yes, there will still be moments where JavaScript makes you stare at the screen for five minutes.
That part is apparently included in the runtime.


Answer to the question from Part 2

We can finally answer the question we left hanging at the beginning.

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

const fn = user.greet;

fn();
// 'Hi, undefined'

setTimeout(user.greet, 100);
// same problem

setTimeout(() => user.greet(), 100);
// works

The first call works differently from what you might expect because this is not permanently attached to the function when the function is defined. For a regular function, this is determined by the call site.

When we write user.greet(), the function is being called as a method of user, so the implicit binding rule gives greet() a this value of user. That is why this.name gives us 'Rohit'.

But then we do this:

const fn = user.greet;

We have taken the function out of the object and stored it in another variable. The function itself is still the same function, but the call site has changed.

fn();

This is now a plain function call. There is no user before the dot, so there is no implicit binding to user. In strict mode, this is undefined. The important point is that the function did not forget user; it was never permanently bound to user in the first place.

The same thing happens here:

setTimeout(user.greet, 100);

We are passing the function itself to setTimeout. The timer receives a reference to the function, not an instruction saying "call this function as user.greet()". When the timer eventually invokes it, the original object relationship is gone.

Now look at the final example:

setTimeout(() => user.greet(), 100);

This time we are not passing user.greet directly. We are passing an arrow function that contains the expression user.greet().

When the timer executes the arrow function, that expression is evaluated exactly as written. The call site once again contains user, so greet() receives user as its implicit this.

So the three cases are really three different call sites.

user.greet();
// `this` is user

const fn = user.greet;
fn();
// no implicit receiver

setTimeout(user.greet, 100);
// the function is passed around and called later

setTimeout(() => user.greet(), 100);
// the wrapper later performs `user.greet()`

And that is the answer we were looking for.

The function did not lose some magical connection to the object. JavaScript never stored that connection in the regular function in the first place. The value of this is determined when the function is called, and changing the way the function is called changes what this refers to.

So if you remember only one thing from this entire detour, remember this:

For regular functions, this comes from the call site, not from where the function was defined.

That one rule is responsible for a ridiculous amount of JavaScript confusion.

And finally, after all that, we can officially close the tab that Part 2 left open.

Understanding an AuTiStiC Language (JavaScript)

Part 3 of 3

Going beyond syntax and figuring out why JavaScript works the way it does with cool examples .

Start from the beginning

Why Does JavaScript Feel So AbSuRd? Part - 1

A weird lowkey autistic language like me .

More from this blog

Why Does JavaScript Feel So AbSuRd?

3 posts

A curious journey into JavaScript uncovering the weird, powerful, and surprisingly logical parts of the language. We’ll go beyond syntax to understand the ideas, history, and machinery behind the code.