skip to content

In JavaScript, what does the identifier `Inner` refer to in `const Outer = class Inner { who() { return Inner.name; } };`, and where is it visible?

level: middleimportance: nice to knowfreq 28%

answer

  1. a class expression can carry its own name
  2. that name does not leak outward
  3. body-only scope, one binding
  4. immutable, and it wins for .name

basics

~10 s

Inner is a binding visible only inside the class body, not in the surrounding scope, where the class is reachable only as Outer. That inner binding is immutable, and Outer.name is the string 'Inner'.

solid answer

~50 s

That is a *named class expression*. The name `Inner` creates a binding in a scope that wraps the class body only: methods, accessors and field initializers can use it, but outside the class `Inner` is simply undeclared — `typeof Inner` there is `"undefined"`. The inner binding is also immutable, so `Inner = null` anywhere in the body throws a `TypeError`, since class bodies are strict code. The payoff is a self-reference that cannot be broken: if someone later reassigns `Outer`, `who()` still resolves the original class. `Outer.name` is `'Inner'` — the explicit name wins. If you omit the name, `const Outer = class {}` still gets `Outer.name === 'Outer'` by inference from the assignment target, but only when there is a target to infer from; `[class {}][0].name` is the empty string. Class expressions are what let a function return a fresh class, or define a one-off class inline.

code

javascript · 14 lines
javascript
let Outer = class Inner {
  who() { return Inner.name; }
};

console.log(Outer.name);          // 'Inner'
console.log(typeof Inner);        // 'undefined' - not visible out here

const first = new Outer();
Outer = class Replacement {};
console.log(first.who());         // 'Inner' - self-reference survives reassignment

const Anon = class {};
console.log(Anon.name);           // 'Anon' - inferred from the binding
console.log([class {}][0].name);  // '' - nothing to infer from

go deeper

for a junior

Know that a class can be written as a value assigned to a variable, and that any name written after the class keyword is only usable inside the class itself.

for a middle

Explain the extra scope the expression name creates, that the binding is immutable, and how name is chosen between an explicit name, an inferred one, and the empty string.

for a senior

Show where you reach for class expressions in real code — factories that manufacture a class per configuration and inline registry entries — and why blank names hurt debuggability.

for a principal

Weigh the readability cost: runtime-generated classes are harder to navigate, review and analyze statically, so require a concrete reason before a codebase adopts them over plain declarations.

## Declaration versus expression `class` appears in two positions. As a statement it is a declaration that binds a name in the enclosing scope: ```js class Widget {} // declaration: binds Widget in this scope const W = class {}; // expression: produces a class value, binds nothing itself const V = class Inner {}; // named class expression ``` An expression is just a value, so it can go anywhere a value can: returned from a function, stored in an array or map, passed as an argument, or immediately constructed with `new (class { ... })()` to get a one-off object with methods on a prototype. ## The inner name binding A *named* class expression creates an extra scope around the class body containing exactly one binding — the class name — initialized to the class itself: ```js const Outer = class Inner { who() { return Inner.name; } }; console.log(new Outer().who()); // 'Inner' console.log(typeof Inner); // 'undefined' - not declared out here ``` Note the last line carefully: `typeof` on a completely undeclared name yields `"undefined"` rather than throwing, so the absence is quiet. `Inner()` or `new Inner()` in the enclosing scope would throw `ReferenceError: Inner is not defined`. This mirrors the older named function expression (`const f = function g() { ... }`), and it exists for the same reason: a value needs a reliable way to refer to itself. ## The binding is immutable Inside the body, the class name is a constant binding. Because every part of a class definition is strict-mode code, an assignment to it is a hard error rather than a silent no-op: ```js const Outer = class Inner { reset() { Inner = null; } // TypeError: Assignment to constant variable. }; ``` That is what makes the self-reference trustworthy. Compare the alternative of referring to `Outer` from inside the body: nothing stops later code from doing `Outer = SomethingElse`, and every self-reference in the class would then follow the reassignment. With `Inner`, factory methods, recursive helpers and identity checks written inside the class keep pointing at the class that actually contains them. ## How `name` is decided The `name` property of the class function comes from three sources, in order: 1. An explicit expression name wins: `const Outer = class Inner {}` → `'Inner'`. 2. Otherwise, an anonymous class expression assigned directly to a variable, a `const`/`let`, a property in an object literal, or a default parameter infers the target's name: `const Outer = class {}` → `'Outer'`; `const obj = { Thing: class {} }` → `obj.Thing.name === 'Thing'`. 3. With no target to infer from, the name is the empty string: `[class {}][0].name === ''`, and the same for a class expression passed straight as an argument. `name` is non-writable but configurable, so tooling can redefine it with `Object.defineProperty`. Anonymous classes with empty names are worth avoiding, because they turn up as blank entries in stack traces, error messages and debugger output. ## Where class expressions earn their keep - **A factory that manufactures classes.** A function can build and return a class configured by its arguments — each call produces a distinct class value with its own prototype object. - **Conditional or table-driven definitions.** `const Impl = fast ? class { ... } : class { ... };` is an ordinary conditional expression; you cannot write that with declarations. - **One-off objects with prototype methods.** `const singleton = new (class { get value() { return compute(); } })();` keeps the shape private and unnameable elsewhere. - **Registries.** Storing classes in a `Map` keyed by a string is just holding values, and class expressions let you define them inline at the point of registration. One trade-off to state out loud: class expressions are not hoisted in any usable form (they are values produced by evaluating an expression), and they are harder to grep for than declarations. Prefer a plain declaration unless you actually need the class to be a value at that spot.

  • Why prefer the inner name over referring to the outer variable from inside the class body?
    The outer variable can be reassigned, shadowed, or simply absent — an anonymous class returned straight from a function has no outer name at all. The inner binding is immutable and always denotes the class that contains it, so self-references, recursive factory methods and identity checks written inside the body stay correct.
  • When does an anonymous class expression end up with an empty `name`?
    When there is no assignment target to infer from: passed directly as an argument, stored into an array or an existing object property via bracket assignment, or returned straight from a function. Name inference only applies to direct assignment forms such as a variable initializer or an object-literal property, so give such classes an explicit name to keep stack traces readable.

saying these in an interview costs you the question

  • Says the expression name is also visible in the enclosing scope
  • Expects typeof Inner outside the class to throw
  • Thinks Outer.name is 'Outer' when the expression is named
  • Assumes reassigning the inner name silently does nothing
  • Believes anonymous classes always get an inferred name

context