Skip to content

On programming with Hardened JavaScript

Erik Marks edited this page Aug 2, 2024 · 1 revision

Classes vs. objects and closures

Consider these two implementations of a counter capability:

// Closure
export const makeCapability = () => {
  let counter = 0;

  return harden({
    inc() {
      counter += 1;
      return counter;
    },
  });
};
harden(makeCapability);

// Class
export class Capability {
  #counter = 0;

  constructor() {
    harden(this);
  }

  inc() {
    this.#counter += 1;
    return this.#counter;
  }
}
harden(Capability);

Are they behaviorally equivalent? Despite appearances:

const cap1 = makeCapability();
const cap2 = new Capability();
console.log(cap1.inc() === cap2.inc()); // true
console.log(cap1.inc() === cap2.inc()); // true
cap1.inc();
console.log(cap1.inc() === cap2.inc()); // false

They in fact differ in at least two important respects:

  1. Methods that depend on private state can be usefully extracted from the closure, but not the class implementation.
    • const { inc } = makeCapability(); inc(); works, but const { inc } = new Capability(); inc(); throws a TypeError.
  2. Class instances can be "hijacked" from a distance.
    • const instance = new Capability(); (new Capability()).inc.call(instance) affects an instance without access to any of its methods.
    • This is because #-private fields are class-private, not instance-private, whereas the closure's private state is instance-private.

Agoric's documentation touches on these topics here. They have also explored the idea of using JavaScript classes as Exo definitions (ref).

Thanks to @gibson042 and @mhofman for elucidating these things.

Clone this wiki locally