skip to content

In a JavaScript class body, where do a field declaration like count = 0 and a method declaration like inc() {} each end up at runtime, and what practical differences follow?

level: middleimportance: must knowfreq 60%

answer

  1. own property vs shared prototype slot
  2. created fresh for each instance
  3. enumerable — so spread and JSON see it
  4. defined, not assigned
  5. inherited setter never fires

basics

~20 s

A class field becomes an own, writable, enumerable, configurable property created on every instance during construction. A method goes once onto the prototype and is shared. So fields appear in Object.keys, spread and JSON output; methods do not.

solid answer

~40 s

Field declarations are per-instance: during construction the engine **defines** an own data property on the new object for each one, in source order, with `writable`, `enumerable` and `configurable` all `true`. Methods are installed once on `Class.prototype` and shared by every instance. That difference is visible everywhere ownership matters — `Object.keys(instance)`, `{ ...instance }`, `JSON.stringify(instance)` and `Object.entries` all show fields and never methods. Two consequences bite in practice. First, memory and cost scale with instance count for fields but not for methods. Second, fields are installed with *define* semantics, not assignment, so a field that shares its name with an inherited accessor **shadows** it with a plain data property instead of calling its setter.

code

javascript · 11 lines
javascript
class Point {
  x = 0;
  y = 0;
  move(dx, dy) { this.x += dx; this.y += dy; }
}

const p = new Point();
console.log(Object.keys(p));                              // [ 'x', 'y' ]
console.log(Object.getOwnPropertyNames(Point.prototype)); // [ 'constructor', 'move' ]
console.log(JSON.stringify(p));                           // {"x":0,"y":0}
console.log({ ...p });                                    // { x: 0, y: 0 }

go deeper

for a junior

Know that values declared in the class body belong to each object while methods are shared, and be able to predict that Object.keys on an instance lists the fields only.

for a middle

Explain the property attributes involved: fields are own, writable, enumerable and configurable; prototype methods are non-enumerable, which is exactly why spread and JSON.stringify treat them differently.

for a senior

Demonstrate judgment about the define-not-assign rule — spot the bug where a subclass field silently shadows a base accessor or wipes state the base constructor set, and know that the fix is assigning in the constructor body.

for a principal

Weigh per-instance materialisation against shared prototype behaviour at scale: object size in hot paths, what stays patchable for instrumentation and tests, and what your serialization boundary should actually expose.

## Two different homes A class body looks uniform, but its declarations land in two different places. ```js class Point { x = 0; // instance field y = 0; // instance field move(dx, dy) { // prototype method this.x += dx; this.y += dy; } } ``` `move` is created once when the class is evaluated and stored on `Point.prototype`. `x` and `y` are created **again for every instance**, as own properties of the object being constructed. ## When field initializers run For a base class, the field initializers run immediately after the instance is created and **before** the constructor body. They run in source order, and each one may use `this`, so a later field can read an earlier one: ```js class Box { width = 4; area = this.width * this.width; // 16 — width already exists } ``` Reversing those two lines yields `NaN`, because `this.width` is still `undefined` when `area` initializes. A field written with no initializer, like `total;`, is still defined — as `undefined`. That matters: it is a real definition, not a no-op declaration. ## The exact property shape Each field becomes a data property with all three attributes set to `true`: ```js Object.getOwnPropertyDescriptor(new Point(), 'x'); // { value: 0, writable: true, enumerable: true, configurable: true } ``` Prototype methods, by contrast, are non-enumerable. That single attribute is why the two show up so differently: ```js const p = new Point(); Object.keys(p); // [ 'x', 'y' ] Object.entries(p); // [ [ 'x', 0 ], [ 'y', 0 ] ] JSON.stringify(p); // '{"x":0,"y":0}' ({ ...p }); // { x: 0, y: 0 } — no move Object.getOwnPropertyNames(Point.prototype); // [ 'constructor', 'move' ] ``` The spread result is the one that surprises people most often: spreading an instance gives you a **plain object with the data and none of the behaviour**. Any code that does `{ ...instance, extra: 1 }` and then calls a method on the result will fail. ## Define, not assign — the shadowing trap Field initialization does not perform `this.x = value`. It performs a property *definition*, the same operation as `Object.defineProperty`. Ordinary assignment walks the prototype chain and hands control to any setter it finds; definition does not. So: ```js class Base { set label(v) { console.log('setter ran with', v); } get label() { return 'from base'; } } class Derived extends Base { label = 'own value'; } const d = new Derived(); // the setter never runs d.label; // 'own value' ``` The derived instance now has an own data property named `label` that hides the inherited accessor completely — the getter is dead too. If your intent was to *feed* the base class's setter, the field declaration is the wrong tool; assign in the constructor body instead (`constructor() { super(); this.label = 'own value'; }`), because that is a real assignment and does find the setter. The same mechanism explains why declaring a field in a subclass silently discards whatever the parent constructor put in that property: the definition runs later and overwrites it. ## Cost and shape Because every field is materialised per instance, a class with many fields makes bigger objects than a class that computes values on demand through prototype accessors. This rarely matters for a handful of objects and can matter a great deal for hundreds of thousands. The reverse is also true for behaviour: everything you can put on the prototype is paid for once. A related consequence is testability and monkey-patching. A prototype method is reachable and replaceable through `Class.prototype.name`, which is how test doubles and instrumentation typically hook in. Anything stored per instance is not reachable that way — you would have to patch each object. ## Quick mental model Ask "is this **state** or **behaviour**?" State that differs per object belongs in fields. Behaviour that is identical for every object belongs on the prototype as a method. The moments to pause are when a piece of behaviour is stored as a field anyway (a bound callback), and when a field's name collides with an inherited accessor — both are cases where the placement rule, not your intent, decides what happens.

  • Why does a field named the same as an inherited setter not call that setter?
    Because field initialization uses property *definition*, the same operation as `Object.defineProperty`, rather than assignment. Assignment consults the prototype chain and delegates to a setter it finds; definition creates an own data property directly on the instance, which then shadows the inherited accessor for reads too. If you want the setter to run, assign in the constructor body instead of declaring a field.
  • What do you get if you spread an instance with object spread?
    A plain object carrying only the instance's own enumerable properties — its fields — with no prototype and therefore no methods. `{ ...user }` produces the data and drops the behaviour, so calling `copy.save()` on the result throws. When you want a real copy of an instance, construct a new one or use a `clone()` method rather than spread.
  • Does a field declared without an initializer, like total;, do anything?
    Yes, and this trips people up. It defines an own property set to `undefined` at initialization time. In a subclass that means it will overwrite whatever the parent constructor already assigned to `total`, because the field definition runs after `super()` returns. If you only wanted documentation, a comment is safer than a bare declaration.

saying these in an interview costs you the question

  • Says class fields are stored on the prototype
  • Thinks methods appear in Object.keys or JSON output
  • Assumes a field assignment triggers an inherited setter
  • Believes spreading an instance copies its methods too
  • Says a bare field declaration does nothing at runtime

context