JavaScript's class keyword is usually described as syntax sugar over prototypes. Which parts really are sugar, which parts cannot be written with the older prototype pattern, and how does that compare with class declarations in other dynamic languages?
answer
- extends + methods on prototype = sugar
- derived ctor does not allocate; super() and new.target do
- subclassing Array/Error only works with class
- #x is not a property: no Proxy, no Object.keys, brand check with #x in obj
- Python __x mangles to _C__x; Ruby private beaten by send
basics
~20 sPrototype wiring and method placement are sugar. Not sugar: derived construction, where the base allocates the instance via new.target so extending Array, Error or Map works; #private fields, which are not properties at all; the this-before-super dead zone; constructors that refuse to be called without new; non-enumerable methods; strict-mode bodies.
solid answer
~60 sSugar: `class C { m(){} }` still produces a function object with `C.prototype.m`, and `extends` still links prototypes. What you cannot reproduce with the ES5 pattern: - **Derived construction.** A derived constructor does not allocate `this`; `super()` does, and the *base* allocates using `new.target`. That is why `class MyArr extends Array` gets real exotic length behaviour, while `function MyArr(){ Array.call(this) }` never could. Before `super()` runs, `this` is in a temporal dead zone and touching it throws. - **`#private` fields.** Not properties: invisible to `Object.keys`, `JSON.stringify`, `Reflect.ownKeys` and Proxy traps, and `#x in obj` is the branding check. Closures gave privacy before, at one function allocation per instance. - Constructors throw if called without `new`; methods are non-enumerable; bodies are strict mode; static initialisation blocks exist. Elsewhere: Python's `class` really is sugar over `type()` with a user-visible metaclass hook, and `__x` name mangling is convention only — `_C__x` is reachable, so Python has no equivalent of `#x`. Ruby classes stay open and `private` only restricts explicit receivers, defeated by `send`. C# and Java classes cannot be reopened at all.
code
javascript · 11 linesclass ClassArr extends Array {}
const a = new ClassArr();
a.push(1);
console.log(a.length); // 1 -- exotic length works
function OldArr() { Array.call(this); }
OldArr.prototype = Object.create(Array.prototype);
const b = new OldArr();
b.push(1);
console.log(b.length); // 1 on the object, but it is a plain object:
console.log(Array.isArray(b)); // falsego deeper
Know that class in JavaScript still creates prototypes underneath and is not a new object model.
Name at least two non-sugar parts, typically private fields and derived construction, and know class bodies are strict mode with non-enumerable methods.
Explain new.target-based allocation, why it makes built-in subclassing work, and the proxy and serialization consequences of private fields.
Compare encapsulation strength across dynamic languages — JavaScript's hard #fields versus Python mangling and Ruby's send — and reason about what a library may safely assume about instances it is given.
## What the sugar claim gets right A JavaScript `class` declaration still produces a function object. Its `prototype` property holds the methods, `extends` sets the prototype links (both the instance chain and the static chain, so static members are inherited too), and instances still delegate through `[[Prototype]]`. `typeof C === 'function'` remains true. Nothing nominal was added: two identical class declarations produce unrelated constructors, and `instanceof` is still a walk over the prototype chain, which is why it fails across realms such as iframes. ## Where the sugar claim stops **Derived construction is genuinely new.** In the ES5 pattern, the derived constructor allocated the object and then called the base as a plain function to initialise it: `function Sub(){ Base.call(this) }`. With `class`, the derived constructor allocates nothing. `super()` invokes the base constructor, which allocates the object using `new.target` — the originally-invoked constructor — and only then is `this` bound in the derived body. Two visible consequences: touching `this` before `super()` throws a `ReferenceError` (a temporal dead zone, not `undefined`), and subclassing exotic built-ins finally works. `class MyArray extends Array` yields an object with real array exotic behaviour, including the magic `length`; the same attempt with `Array.call(this)` produced an ordinary object that merely inherited array methods and never tracked `length`. The same applies to `Error` (a proper stack and `instanceof`), `Map`, `Set` and `Promise`. **Private fields are not properties.** `#count` lives in a separate per-class slot namespace. It cannot be reached by `Object.keys`, `JSON.stringify`, `Object.getOwnPropertyNames` or `Reflect.ownKeys`, and it is invisible to a `Proxy` — a proxy wrapping an instance cannot forward private-field access, which is a real interop constraint. Accessing `#x` on an object that does not have it throws a `TypeError`, and `#x in obj` was added as the safe brand check. Before this, privacy meant closures: real, but costing one function object per instance per method, and unavailable to prototype methods. **Smaller but real differences.** A class constructor throws if called without `new`. Class methods are non-enumerable, so they do not show up in `for...in`, unlike hand-assigned `Ctor.prototype.m = ...`. Class bodies are always strict mode. Declarations are hoisted but in a temporal dead zone, so using the name before the declaration throws instead of yielding `undefined`. Static blocks allow initialisation logic. Fields declared in the class body are defined per instance, in order, before the constructor body runs (for a base class). ## The comparison that makes the point Python's `class` statement is much closer to pure sugar: it executes the body to build a namespace dict and calls the metaclass, normally `type(name, bases, ns)`. You can do the same by calling `type()` yourself, and the metaclass hook is a supported extension point. Everything stays a dict entry, so classes remain mutable and monkeypatchable, and 'private' is only the `__x` name-mangling convention producing `_ClassName__x` — trivially reachable, which is why Python has nothing comparable to `#x`. Ruby is more open still: classes are reopened as an idiom, `private` restricts calls with an explicit receiver rather than hiding the method, and `send` bypasses it. C#, Java and Kotlin sit at the other end: a class is fixed when compiled, cannot be reopened, and private is a real access boundary enforced by the compiler. So JavaScript's `class` occupies a genuinely odd position. It reads nominally, and it added the first non-forgeable encapsulation to the language, while everything else remains one mutable object graph you can still reach into and reassign at run time. Understanding which half is which decides real things: whether a library can proxy your instances, whether a serializer will see a field, whether a subclass of a built-in behaves like the built-in, and whether hot-patching a method at run time is a supported operation or an accident waiting for the next release.
- Why can a Proxy not forward access to a private class field of the object it wraps?Private fields are not properties, so they never reach the get/set traps; they are looked up in a per-class slot keyed by the actual instance. The proxy is a different object without that slot, so a method invoked with the proxy as this throws a TypeError. The workaround is to make the proxy forward method calls to the real instance (binding this to the target) rather than proxying field access.
- If class is mostly sugar, why does instanceof still fail across iframes or realms?Because instanceof walks the prototype chain looking for the constructor's prototype object, and each realm has its own copies of the built-ins and of your class. The class keyword changed nothing about that; it is still identity of prototype objects. Cross-realm checks use structural or brand tests such as Array.isArray or Symbol.hasInstance instead.
saying these in an interview costs you the question
- Claiming class is purely cosmetic, so the ES5 pattern can do everything including subclassing Array or Error.
- Saying #private is just a naming convention like Python's underscore; it is a separate namespace enforced by the language.
- Expecting this to be usable before super() in a derived constructor, or to be undefined rather than throwing.
- Assuming class methods are enumerable, or that a class constructor can be invoked without new.
- Believing a Proxy can intercept private-field access, or that JSON.stringify will serialize such fields.