In JavaScript, when you write `class Point { constructor(x) {} move(dx) {} }`, where does `move` actually live, and how does that differ from writing `Point.prototype.move = function (dx) {}` in the older constructor-function style?
answer
- same place, different descriptor
- one style leaks into for...in
- enumerable is false on class methods
- assignment sets enumerable true
- no [[Construct]] slot on class methods
basics
~20 sBoth forms put move on Point.prototype, so lookup is identical. The class defines it as non-enumerable and non-constructible, so for...in and Object.assign skip it, while the hand-written assignment creates an enumerable, constructible property they both pick up.
solid answer
~40 sBoth forms end up with `move` as a property of `Point.prototype`, shared by every instance rather than copied onto each one — that part genuinely is sugar. The difference is the property descriptor. A class method is installed the way `Object.defineProperty` would install it: `enumerable: false`, `writable: true`, `configurable: true`. The hand-written `Point.prototype.move = ...` is a plain assignment, so it is enumerable, and it therefore shows up in `for...in` over an instance and in `Object.assign(target, Point.prototype)`; the class method shows up in neither. Class methods also lack a `[[Construct]]` slot, so `new p.move()` throws a `TypeError`, whereas the assigned function expression is constructible and even carries its own unused `.prototype` object. Finally, a class's own `prototype` property is non-writable, so you cannot replace it wholesale. `Point.prototype.constructor` still points back at `Point` in both.
code
javascript · 14 linesclass Point { move() {} }
function Legacy() {}
Legacy.prototype.move = function () {};
console.log(Object.getOwnPropertyDescriptor(Point.prototype, 'move').enumerable); // false
console.log(Object.getOwnPropertyDescriptor(Legacy.prototype, 'move').enumerable); // true
const keys = (o) => { const out = []; for (const k in o) out.push(k); return out; };
console.log(keys(new Point())); // []
console.log(keys(new Legacy())); // ['move']
console.log(Object.assign({}, Point.prototype)); // {}
console.log(Object.assign({}, Legacy.prototype)); // { move: [Function] }go deeper
Be ready to say that methods written in a class body live on the prototype and are shared by every instance, and that a class is still a function under the hood.
Explain the descriptor concretely: defined rather than assigned, so enumerable is false while writable and configurable stay true, and show the for...in difference that follows.
Demonstrate the bugs you have actually hit — prototype-copying helpers and mixin utilities that silently produce nothing once the source becomes a class, and methods that cannot be invoked with new.
Own the API consequence: non-enumerable-by-default is what keeps class-based objects safe for code that copies enumerable properties, so treat defining methods by plain assignment as a deliberate, justified exception.
## What the declaration actually creates A class declaration creates exactly one function object. `class Point { constructor(x) { this.x = x } move(dx) { this.x += dx } }` produces a function named `Point` whose body is the `constructor` body; every other method in the class body becomes a property of `Point.prototype`; `Point.prototype.constructor` refers back to `Point`; and `new Point(1)` produces an object whose internal prototype is `Point.prototype`, so `p.move(2)` is resolved by walking the prototype chain rather than found on the instance. `typeof Point` is `"function"`. That is the same object graph the pre-class pattern builds by hand: ```js function Point(x) { this.x = x; } Point.prototype.move = function (dx) { this.x += dx; }; ``` So the shape is identical. What differs is *how* the method property is installed, and a handful of restrictions the class form adds. ## The descriptor difference A method in a class body is defined, not assigned — the effect is the same as `Object.defineProperty(Point.prototype, 'move', { value: fn, writable: true, enumerable: false, configurable: true })`. A hand-written `Point.prototype.move = fn` is an ordinary assignment, and assignment creates a data property with `writable`, `enumerable` and `configurable` all `true`. The single practical bit is `enumerable`: ```js Object.keys(Point.prototype); // [] Object.getOwnPropertyNames(Point.prototype); // ['constructor', 'move'] ``` ## Why non-enumerability matters `for...in` iterates enumerable string-keyed properties *including inherited ones*. With the ES5 pattern, `for (const k in p)` yields `'move'` alongside the instance's own data — which is exactly why old code is littered with `if (!obj.hasOwnProperty(k)) continue;`. With a class, the loop yields only the instance's own properties, and the guard is unnecessary. The same rule drives copying helpers. `Object.assign` and object spread copy *own enumerable* properties, so `Object.assign({}, Legacy.prototype)` carries the methods over, while `Object.assign({}, Point.prototype)` copies nothing at all. Anyone who wrote a hand-rolled "mix these methods in" helper on top of `Object.assign` finds it silently produces empty objects the day the source becomes a class. The fix is to read keys with `Object.getOwnPropertyNames` (or `Reflect.ownKeys`, which also returns symbols) and copy descriptors with `Object.getOwnPropertyDescriptor` / `Object.defineProperty`. The class default is not arbitrary: the built-ins behave the same way. `Array.prototype.map` and `Object.prototype.toString` are non-enumerable, which is why `for...in` over an array does not list `map`. Class syntax simply made user code match the platform. ## Methods are not constructors A method defined in a class body (like a shorthand method in an object literal) has no `[[Construct]]` internal method: ```js const p = new Point(1); new p.move(); // TypeError: p.move is not a constructor ``` The assigned function expression in the ES5 version *is* constructible, and it additionally allocates its own `.prototype` object that nothing ever uses. Class methods also have no `prototype` property at all. ## The prototype property itself For an ordinary function, `Foo.prototype` is `{ writable: true, enumerable: false, configurable: false }` — the classic `Foo.prototype = { ... }` wholesale replacement works (and quietly loses `constructor` unless you restore it). For a class, `prototype` is `{ writable: false, enumerable: false, configurable: false }`, so `Point.prototype = {}` throws a `TypeError` in strict code and fails silently in sloppy code. You can still patch individual members — class methods are writable and configurable, so `Point.prototype.move = betterMove` and `delete Point.prototype.move` both work. ## What is genuinely the same Method lookup through the chain, `instanceof`, the single shared copy of each method, and the ability to monkey-patch the prototype after the fact are all unchanged. The mental model "a class is a constructor function plus a prototype object" stays correct; what you must add is "…whose methods are defined rather than assigned, and whose function object refuses a few things an ordinary function allows."
- If class methods are non-enumerable, how do you list them at run time?Use `Object.getOwnPropertyNames(C.prototype)`, which ignores enumerability, and drop `'constructor'`; add `Object.getOwnPropertySymbols` or use `Reflect.ownKeys` if symbol-keyed methods matter. To include inherited methods, repeat that on each object returned by `Object.getPrototypeOf` until you reach `Object.prototype`.
- Does `JSON.stringify` of an instance differ between the two styles?No. `JSON.stringify` serializes only own enumerable string-keyed properties, and prototype methods are not own properties in either style — function values are skipped anyway. The observable differences are `for...in`, `Object.assign`/spread over the prototype object, and `Object.keys(C.prototype)`.
- Why did the language make class methods non-enumerable rather than matching assignment?It matches the built-ins: `Array.prototype.map` and friends have always been non-enumerable, so instances iterate cleanly. Enumerable prototype methods were a long-standing nuisance — every `for...in` loop needed a `hasOwnProperty` guard, and property-copying helpers dragged behaviour along with data.
saying these in an interview costs you the question
- Says each instance gets its own copy of the method
- Claims classes replaced prototypes with a different object model
- Assumes Object.assign copies methods off a class prototype
- Thinks for...in never walks the prototype chain
- Believes any function-valued property can be called with new