Javascript without "this"

2021-03-29

Javascript's this keyword is problematic for several reasons:

  1. It is hard to discuss this without confusion.
  2. If you forget a this you can unintentionally create, or access, variables in the global scope.
  3. Because it can be rebound, you cannot tell what value this has until runtime.

To demonstrate point 2:

let bar = "my special value";
function foo(n) {
  this.bar = 2;
  // Here we forget the this. prefix on bar, accessing instead of global bar.
  this.baz = Math.pow(n, bar);
  // Here we create a var, but it isn't local to our function, it is global.
  tmp = "hello world";
  return baz;
}
foo(1); // → NaN
console.log(tmp); // -> "hello world"

To demonstrate the third point:

js
function currency(amount) {
  prefix = "$";
  return this.prefix + parseFloat(amount).toFixed(2);
}
currency("10"); // → '$10.00'
const currency2 = currency.bind({ prefix: "€" });
currency2("10"); // → '€10.00'
currency.call({}, "5"); // → 'undefined5.00'

It is much easier to reason about code when referential transparency is maintained — the code as written should be sufficient to reason about the system without needing to know what data is bound at runtime.

How do we avoid this?

The simple & contrived examples above are easy to re-write without this:

let bar = "my special value";
function foo(n) {
  const bar = 2;
  return Math.pow(n, bar);
}
function currency(amount) {
  const prefix = "$";
  return prefix + parseFloat(amount).toFixed(2);
}

It becomes more challenging when you need a group of functions that share state:

const Queue = function () {
  this.items = {};
  this.head = 0;
  this.tail = 0;

  this.enqueue = function (item) {
    this.items[this.tail] = item;
    this.tail++;
  };

  this.dequeue = function () {
    const item = this.items[this.head];
    delete this.items[this.head];
    this.head++;
    return item;
  };

  this.peek = function () {
    return this.items[this.head];
  };

  this.length = function () {
    return this.tail - this.head;
  };
};

When this is the case — when you need isolated state accessible to a group of functions, a closure is the solution.

const makeQueue = () => {
  let items = {};
  let head = 0;
  let tail = 0;

  const enqueue = (item) => {
    items[tail] = item;
    tail++;
  }

  const dequeue = () => {
    const item = items[head];
    if (item === undefined) {
      return item;
    }
    delete items[head];
    head++;
    return item;
  }

  const peek = () => {
    return items[head];
  }

  const length = () => {
    return tail - head;
  }

  return Object.freeze({enqueue, dequeue, peek, length});
}