skip to content

A JavaScript constructor function stores its first instance on a property of itself and returns that stored object on every later call, so `new Config()` always yields the same object. How does that work, and what problems does it cause in production code?

level: seniorimportance: nice to knowfreq 28%

answer

  1. one rule about returned objects makes it work
  2. the freshly built object is thrown away
  3. later arguments never reach anything
  4. process-wide state that tests must reset
  5. a subclass gets the parent's cached object

basics

~20 s

It exploits the rule that an object returned from a constructor replaces the freshly built this, so every call after the first hands back the cached instance. The costs are silently ignored constructor arguments, hidden global state that tests cannot reset, and subclasses that receive the base instance instead of their own.

solid answer

~50 s

The constructor checks a cache slot first - typically a property on the constructor function itself - and `return`s the stored object if it exists. Because a returned **object** overrides the `this` that `new` created, the caller gets the cached instance and never learns a new one was built and thrown away. It works, and it is the canonical "real" use of the return-override rule, but it lies to callers: `new` normally promises a fresh object. Concretely, arguments passed to every call after the first are silently discarded, the cache is process-wide state that tests must reach into to reset, and a subclass is broken - `super()` returns the cached base instance, so `new Sub()` yields an object that fails `instanceof Sub` and lacks the subclass's prototype methods and fields. I would expose an explicit `getConfig()` factory or a module-level object instead, so nothing pretends to construct.

code

javascript · 13 lines
javascript
function Config(options) {
  if (Config.instance) return Config.instance;
  this.options = options;
  Config.instance = this;
}

const a = new Config({ mode: 'a' });
const b = new Config({ mode: 'b' });
console.log(a === b, b.options.mode); // true 'a'

class Extended extends Config {}
const e = new Extended({ mode: 'c' });
console.log(e === a, e instanceof Extended); // true false

go deeper

for a junior

Recognise the shape: the constructor returns a stored object, and because object returns override this, every call yields the same instance. Be able to read such code and predict that two new calls compare equal.

for a middle

Explain each step - the fresh object is still built and then discarded, the cache lives on the constructor function itself, and instanceof still passes because the cached object really was constructed by it. Point out that later arguments are ignored.

for a senior

Demonstrate the production judgment: name the subclass failure, the import-order dependence of who configures the instance, and the testing problem of process-wide state, then propose an explicit factory or module-level export that does not deceive callers of new.

for a principal

Argue about where single-instance state belongs at all: process-wide caches interact badly with parallel tests, multiple bundles of the same library, and per-request isolation on a server. Decide whether the lifetime should be owned by a container or a request scope rather than by a constructor.

## The mechanism The trick rests entirely on one rule: when a constructor's body returns an object, `new` hands back that object instead of the `this` it created. ```js function Config(options) { if (Config.instance) return Config.instance; this.options = options; Config.instance = this; } const a = new Config({ mode: 'a' }); const b = new Config({ mode: 'b' }); a === b; // true b.options.mode; // 'a' ``` The first call takes the normal path: a fresh object is created, populated, and stored on the constructor function (which is itself just an object and can carry properties). Every later call creates a fresh `this`, immediately abandons it via the `return`, and gives the caller the cached one. The abandoned objects are garbage - a small but real cost if the constructor is called in a loop. A variant caches per key rather than a single instance, giving interning: ```js function Currency(code) { const cached = Currency.cache.get(code); if (cached) return cached; this.code = code; Currency.cache.set(code, this); } Currency.cache = new Map(); new Currency('EUR') === new Currency('EUR'); // true ``` ## Why `instanceof` still passes for the base The cached object was genuinely produced by `new Config(...)`, so its `[[Prototype]]` is `Config.prototype` and `cached instanceof Config` is `true`. That is what makes the trick feel harmless: the type test the caller would run still succeeds. ## Where it breaks: subclassing This is the failure worth being able to name. ```js class Extended extends Config {} const e = new Extended({}); e === a; // true - the cached base instance e instanceof Extended; // false - its prototype is Config.prototype ``` A derived constructor's `this` is whatever the parent construction produced. Because the parent returned the cached object, that object becomes the derived `this` and is what `new Extended()` evaluates to. It was created before `Extended` existed, so it is linked to `Config.prototype`: none of `Extended`'s prototype methods are reachable, none of its class fields were installed, and the `instanceof` test that callers rely on returns `false`. The failure is silent until someone calls a subclass method. ## The other production costs - **Arguments are silently ignored.** `new Config({ mode: 'b' })` looks like it configured something. It configured nothing. There is no error, no warning - just the first caller's options, forever. Whichever module happens to import and construct first wins, which makes behaviour depend on import order. - **Hidden global state.** The cache lives for the lifetime of the process (or the page). Tests that need a clean instance must reach into the implementation and delete `Config.instance`, coupling the test suite to a private detail. Parallel tests sharing a process interfere with each other. - **Unbounded keyed caches leak.** The per-key variant keeps a strong reference to every instance ever created; if keys come from user input, the map grows without limit. - **It violates the contract of `new`.** Every reader of `new X()` assumes a fresh object. Code that mutates "its" instance, or compares two instances for identity, quietly operates on shared state. ## What to do instead Make the sharing visible at the call site rather than hiding it behind `new`: ```js let instance; export function getConfig(options) { instance ??= new Config(options); return instance; } ``` or, simpler still in a module system where a module body evaluates once, export a configured object directly. Both remove the lie: nobody writes `new` and receives something old. If a class must remain the public surface, a `static create()` or a static registry method is clearer than an invisible override inside the constructor. ## How this is asked Usually as "can you make `new Foo()` always return the same object?" followed by "what would go wrong?". Producing the code is the easy half; the discriminating half is naming the subclass breakage, the silently discarded arguments, and the testability problem, then proposing the explicit factory. Candidates who present the trick as a best-practice singleton pattern without any of those caveats read as inexperienced.

  • Does `instanceof` still behave correctly with this pattern?
    For the constructor itself, yes - the cached object was really built by it, so its prototype chain is intact and `cached instanceof Config` is `true`. For a subclass it fails: `super()` returns the cached base instance, which was linked to `Config.prototype` before the subclass existed, so `new Sub() instanceof Sub` is `false` and the subclass's prototype methods and fields are missing.
  • How would you get the same single-instance guarantee without the constructor-return trick?
    Expose an explicit accessor - a `getConfig()` function or a `static create()` that lazily builds and memoises the instance - or simply export a ready-made object from a module whose body evaluates once. Both make the sharing visible at the call site, keep `new` honest for anyone who genuinely wants a fresh object, and give tests an obvious seam to reset or substitute.
  • What is the memory behaviour of the keyed variant that caches one instance per key?
    The cache holds a strong reference to every instance it has ever created, so none of them can be collected while the cache lives - which is usually the whole process. That is fine for a small fixed key space like currency codes, and a leak if keys derive from user input or request data. Bound the cache or key it in a way that allows collection.

saying these in an interview costs you the question

  • Presents it as the standard singleton pattern with no caveats
  • Thinks later constructor arguments still update the instance
  • Assumes subclasses inherit the caching behaviour correctly
  • Believes no object is created on the cached calls
  • Claims module-level state is automatically test-friendly

context