Skip to main content

Command Palette

Search for a command to run...

Why Does JavaScript Feel So AbSuRd? Part - 2

A weird lowkey autistic language like me .

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

If Part 1 was about JavaScript doing suspicious things with values and bullshit like that , Part 2 is where we meet the thing that makes JavaScript feel like an actual programming language and, occasionally, like a homeless kid (orphan) !

Let's take an example.

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

Can you answer why the first two calls fail while the last one works? If you can, probably this blog is not for you.

But if you can't, then it's completely up to you whether you want to read this article or continue staring at this until it starts staring back.

The interesting part is that all three examples involve the same function.

Functions are everywhere in JavaScript. They can be stored in variables, passed to other functions, returned from functions, attached to objects, blah blah blah. Once you understand that functions are values and that JavaScript keeps track of the environment around those functions, a lot of features that initially look unrelated start fitting together.

This part follows that idea from the ground up. We will move through function forms, parameters, first class functions, higher order functions, reduce(), currying, composition, pipe(), scope, hoisting, closures, practical closure patterns, and finally the infamous this .


What exactly is a function in JavaScript?

A function is a value that represents executable behaviour. That sounds simple, but the important part is the word value.

Because functions are values, you can assign them to variables, store them in objects or arrays, pass them as arguments, and return them from other functions. This is the foundation for higher-order functions, callbacks, composition, currying, and a huge amount of modern JavaScript.

A function can be written in several forms, and the form you choose can change things such as hoisting, this, constructor behaviour, and whether the function has its own arguments object.

// Function expression
const subtract = function (a, b) {
  return a - b;
};

// Arrow function
const multiply = (a, b) => a * b;

const square = x => x * x;

// Function declaration
function add(a, b) {
  return a + b;
}

// Named function expression
const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);
};

The important distinction is that a function declaration is fully hoisted, while a function expression is created as part of the assignment. Arrow functions are also function expressions and have different behaviour around this, arguments, constructors, and prototypes.

JavaScript also has specialised forms such as async functions, generator functions, and async generators.

async function fetchData() {
}

function* idGenerator() {
  let i = 0;

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

async function* streamPages() {
}

So when someone says "a function is a function" screw them up , they don't know.


Why does the distinction between regular functions and arrow functions matter?

Because an arrow function is not simply a shorter way of writing a regular function.

The biggest difference is this. A regular function gets its this value from the way it is called. An arrow function does not create its own this; it takes this from the surrounding lexical scope.

Regular functions also have their own arguments object and can be used with new. Arrow functions cannot be constructors and do not have their own arguments.

const counter = {
  count: 0,

  // Wrong if you expect `this` to mean counter
  incrementBad: () => {
    this.count++;
  },

  // Correct
  increment() {
    this.count++;
  },

  startBad() {
    setInterval(function () {
      this.count++;
    }, 1000);
  },

  start() {
    setInterval(() => {
      this.count++;
    }, 1000);
  },
};

The usual rule is simple enough to remember. If a function is an object or class method and needs the object's this, use a regular function. If you are writing a callback inside that method and want to keep the surrounding this, an arrow function is often exactly what you want.


What are default parameters?

Default parameters allow a function to use a fallback value when the corresponding argument is undefined.

The default is evaluated when the function is called, not when the function is defined, and parameter defaults are evaluated from left to right.

function greet(name = 'Guest', greeting = `Hello, ${name}`) {
  return greeting;
}

greet();
// 'Hello, Guest'

greet('Rohit');
// 'Hello, Rohit'

One detail that catches people is that the default only applies to undefined, not to every falsy value and not to null.

function f(x = 5) {
  return x;
}

f(undefined);
// 5

f(null);
// null

So null basically says, "I intentionally gave you null." JavaScript respects that decision.


What are rest parameters?

Rest parameters collect the remaining arguments into a real array.

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

sum(1, 2, 3, 4);
// 10

This is different from the older arguments object because rest parameters give you an actual array, so array methods can be used directly.

You can also combine a normal parameter with rest parameters.

function log(level, ...messages) {
  console[level](...messages);
}

Here level receives the first argument and messages collects everything after it.


Why do we sometimes write = {} in a destructured parameter?

This pattern is common when a function accepts an options object.

function createServer({
  port = 3000,
  host = 'localhost',
  secure = false
} = {}) {
  return `${secure ? 'https' : 'http'}://${host}:${port}`;
}

createServer();
// 'http://localhost:3000'

createServer({ port: 8080 });
// 'http://localhost:8080'

The final = {} is important because without it, calling createServer() would try to destructure undefined, which is not possible.

The inner defaults handle missing properties. The outer default handles the case where the entire argument is missing.

That tiny = {} is doing more work than it looks like.


First-Class Functions

What does it mean when people say "functions are first-class citizens"?

It means functions can be treated like ordinary values.

You can assign a function to a variable, pass it to another function, return it from a function, or store it inside a data structure.

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

const operation = add;

operation(2, 3);
// 5

You can also pass a function into another function.

function execute(fn, value) {
  return fn(value);
}

execute(x => x * 2, 5);
// 10

And you can return a function.

function createMultiplier(multiplier) {
  return value => value * multiplier;
}

const double = createMultiplier(2);

double(5);
// 10

This ability to move functions around as values is one of the central ideas behind functional programming in JavaScript.


What is a higher order function?

A higher-order function is a function that takes another function as an argument, returns a function, or does both.

For example, we can create a wrapper that adds logging around another function.

const withLogging = fn => (...args) => {
  console.log(`calling ${fn.name} with`, args);

  const result = fn(...args);

  console.log(`${fn.name} returned`, result);

  return result;
};

const loggedAdd = withLogging(add);

loggedAdd(2, 3);

withLogging is higher-order because it receives a function and returns another function.

This pattern is extremely useful because you can add behaviour without modifying the original function. Logging, authentication checks, caching, timing, retry logic, debouncing, throttling, and many other patterns can be built this way.A higher order function is a function that takes another function as an argument, returns a function, or does both.


Why does reduce() matter so much?

reduce() takes a collection and repeatedly combines its elements into one accumulated result.

The accumulator is the key idea. On each iteration, the callback receives the result produced so far and the current element.

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

const total = numbers.reduce(
  (accumulator, number) => accumulator + number,
  0
);

console.log(total);
// 10

The first accumulator value is 0. JavaScript then processes the array one element at a time, carrying the previous result into the next iteration.

More importantly for functional programming, reduce() can be used to build a result by repeatedly applying functions. That idea leads directly to composition and pipe().


What is currying?

Currying transforms a function that normally accepts several arguments into a sequence of functions that each accept arguments progressively.

nstead of this:

add3(1, 2, 3);

you can have:

add3(1)(2)(3);

A simple curry implementation can be written like this.

const curry = fn =>
  function curried(...args) {
    return args.length >= fn.length
      ? fn(...args)
      : (...more) => curried(...args, ...more);
  };

const add3 = curry((a, b, c) => a + b + c);

add3(1)(2)(3);
// 6

add3(1, 2)(3);
// 6

The returned function remembers the arguments collected so far and waits until enough arguments have been provided.

This implementation depends on fn.length, which is the number of declared parameters before the first default or rest parameter.

function add(a, b, c) {
}

console.log(add.length);
// 3

function another(a, b = 10, c) {
}

console.log(another.length);
// 1

function many(a, ...numbers) {
}

console.log(many.length);
// 1

That is why this simple curry implementation is useful for learning but should not be treated as a universal currying library.


Composition

What is function composition?

Composition means combining small functions so that the output of one function becomes the input of another.

Suppose we have three transformations.

const trim = value => value.trim();

const lower = value => value.toLowerCase();

const removeSpaces = value =>
  value.replace(/\s+/g, '-');

Instead of manually calling them one by one, we can create a new function that represents the entire operation.

const compose = (...fns) => x =>
  fns.reduceRight(
    (acc, fn) => fn(acc),
    x
  );

compose() applies functions from right to left.

const transform = compose(
  removeSpaces,
  lower,
  trim
);

transform('  Hello World  ');
// 'hello-world'

The value enters trim, then moves into lower, and finally reaches removeSpaces.

This style works particularly well when each function performs one small, predictable transformation.


Then what is pipe() ?

pipe() is essentially the left-to-right version of composition.

Instead of reading the functions from the right side backwards, you read them in the same order the data flows through them.

const pipe = (...fns) => x =>
  fns.reduce(
    (acc, fn) => fn(acc),
    x
  );

Now we can build something that reads almost like a sequence of instructions.

const slugify = pipe(
  s => s.trim(),
  s => s.toLowerCase(),
  s => s.replace(/[^a-z0-9]+/g, '-'),
  s => s.replace(/^-|-$/g, '')
);

slugify('  Hello, World!  ');
// 'hello-world'

This is one of the places where reduce() becomes much more interesting. The accumulator is not necessarily a number. It can be the value moving through a chain of transformations.


What is scope?

Scope determines where a variable can be accessed.

const globalVar = 'global';

function outer() {
  const functionVar = 'function';

  if (true) {
    let blockVar = 'block';
    console.log(blockVar);
  }

  var functionScoped = 'also function';

  console.log(functionScoped);

  function inner() {
    console.log(globalVar, functionVar);
  }

  inner();
}

The important difference is that var is function scoped, while let and const are block scoped.

function example() {
  if (true) {
    var a = 1;
    let b = 2;
  }

  console.log(a);
  // 1

  console.log(b);
  // ReferenceError
}

The braces create a block scope for let and const, but var ignores that block and belongs to the surrounding function scope.


What is the scope chain?

When JavaScript encounters a variable reference, it looks for that variable in the current lexical environment. If it is not there, it moves outward through the enclosing scopes until it reaches the global scope.

const a = 'global a';

function level1() {
  const b = 'level1 b';

  function level2() {
    const c = 'level2 c';

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

    level3();
  }

  level2();
}

level1();

When level3() looks for a, it does not find it locally, so JavaScript walks outward. It finds a in the global scope. The same process finds b in level1 and c in level2.

This is called the scope chain.

The critical point is that scope is lexical. It is determined by where the code is written, not by where the function happens to be called from.

That rule is one of the reasons closures work.


OHH !! I totally forgot to answer the question I asked at the very beginning of this discussion! 😭

But to get to that answer, we still have a long way to go and I think this is enough for this part.

To see the answer, you’ll have to wait for the next one.

See you in the next part!

Understanding an AuTiStiC Language (JavaScript)

Part 2 of 3

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

Up next

Why Does JavaScript Feel So AbSuRd? Part - 3

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.