In a JavaScript class, what actually differs between defining `handleClick = () => { ... }` as an instance field and defining `handleClick() { ... }` as a prototype method?
answer
- own property versus prototype property
- one closure per instance
- enumerable, so spread and Object.keys see it
- own property shadows subclass override
- prototype patching cannot reach it
basics
~20 sThe arrow field is an own, enumerable property created fresh on every instance, with this locked to that instance. The method lives once on the prototype, is non-enumerable, and takes this from the receiver at call time.
solid answer
~40 sAn arrow class field is an initializer that runs during construction, so each instance gets its own function object whose `this` is permanently that instance — which is why extracted references like `onClick={obj.handleClick}` keep working. The costs are real: one closure allocated per instance instead of one shared function; the property is an own enumerable property, so it appears in `Object.keys` and `JSON.stringify`; it shadows any same-named prototype method, so a subclass method of that name is silently ignored and monkey-patching `Cls.prototype.handleClick` in a test has no effect on instances. A prototype method is shared, non-enumerable, overridable by subclasses, reachable via `super`, and cheap — but it loses `this` when detached. Choose the field when a detached reference is a hard requirement, the method otherwise.
code
javascript · 14 linesclass Base {
greet = () => 'base field';
hello() { return 'base method'; }
}
class Sub extends Base {
greet() { return 'sub method'; }
hello() { return 'sub method'; }
}
const s = new Sub();
console.log(s.greet()); // 'base field' - own field shadows the override
console.log(s.hello()); // 'sub method' - normal prototype override
console.log(Object.keys(s)); // [ 'greet' ] - fields are enumerable own propsgo deeper
Know that an arrow class field keeps this pointing at the instance even when the function is passed elsewhere, while a plain method loses this once detached from the object.
Explain where each lands — own property created per instance versus one shared, non-enumerable property on the prototype — and that the field initializer runs during construction with this already set.
Weigh the real costs in a running system: per-instance allocation, enumerability leaking into spread and serialization, prototype patching going inert in tests, and a base field silently shadowing a subclass override.
Set the house rule and the escape hatch: prototype methods by default for shareability and overridability, arrow fields only where a detached reference is a hard requirement, and say how you would keep that boundary enforceable across a large codebase.
## Two genuinely different objects The two syntaxes look interchangeable in a class body, but they produce different property locations, different lifetimes, and different `this` semantics. ```javascript class Widget { asField = () => this; // own property of each instance asMethod() { return this; } // one function on Widget.prototype } ``` `asMethod` is installed once on `Widget.prototype` when the class is evaluated. `asField` is an *initializer expression* that is evaluated once per construction, in field declaration order, after `super()` returns in a derived class. Each `new Widget()` therefore allocates a brand-new arrow closure. ## `this` The field initializer runs with `this` already bound to the instance being constructed, and the arrow captures it lexically. That binding cannot be changed afterwards by `call`, `apply`, `bind`, or by how the function is later invoked. The prototype method has no such capture: `this` is whatever receiver the call supplies, which is why `const f = w.asMethod; f()` yields `undefined` in the strict-mode class body. This is the entire reason the arrow-field idiom exists: it survives detachment. ```javascript const w = new Widget(); const a = w.asField, m = w.asMethod; a() === w; // true m(); // undefined - receiver lost ``` ## Memory and identity One function object per instance per field. For a handful of long-lived objects that is noise; for tens of thousands of short-lived ones it is measurable allocation and retained closure state. Identity differs too: `w1.asField !== w2.asField`, while `w1.asMethod === w2.asMethod`. Any code that memoizes on function identity, dedupes listeners, or compares props sees two different values for two instances. ## Enumerability and serialization Class instance fields are created with `CreateDataPropertyOrThrow`, so they are writable, enumerable and configurable own properties. Prototype methods are non-enumerable. Consequences that bite in production: ```javascript Object.keys(new Widget()); // ['asField'] JSON.stringify(new Widget()); // '{}' - functions are dropped, but the key was visited ``` A `for...in` loop, a spread `{ ...instance }`, a shallow-clone helper, or a diffing routine will all see `asField`. Spreading an instance copies the arrow across to a plain object — where it still points at the original instance, which is a genuinely confusing outcome. ## Shadowing, overriding, and `super` Own properties win over prototype properties in lookup. So a base-class arrow field shadows a subclass's prototype method of the same name — the subclass override never runs: ```javascript class Base { greet = () => 'base field'; } class Sub extends Base { greet() { return 'sub method'; } } new Sub().greet(); // 'base field' ``` A subclass can only override by declaring its own field of that name, because derived field initializers run after the base's and overwrite the own property. And `super.greet()` cannot reach a base-class *field*: `super` looks up the prototype chain, where the field never was. ## Testing and patching A test that replaces `Widget.prototype.handleClick` changes behaviour for methods and is completely inert for fields — every existing and future instance carries its own copy. Instrumentation, decorators applied to prototype methods, and mocking libraries that patch prototypes all lose their grip on arrow fields. Patching must move to the instance. ## How to decide Prefer the prototype method by default: shared, cheap, non-enumerable, overridable, patchable, visible on the prototype for tooling. Reach for the arrow field only when a detached reference is a hard requirement and the alternatives are worse — the alternatives being binding once in the constructor (`this.handleClick = this.handleClick.bind(this)`, which has the same per-instance cost but keeps a prototype method available for `super` and patching) or wrapping at the call site with an arrow, which keeps everything shared but allocates per call site. The senior signal in this question is not picking a side. It is naming which of the four axes — `this` capture, allocation, enumerability, overridability — actually matters for the code in front of you, and noticing that the arrow field trades three of them away to buy one.
- A test replaces `Widget.prototype.handleClick` with a spy, and the spy never fires. What is the likely cause?`handleClick` is an arrow class field, so every instance holds its own copy as an own property. Property lookup finds that own property and never reaches the prototype, making the prototype patch inert. Fix it by patching the instance directly, or by converting the field to a prototype method and binding in the constructor if detachment is needed.
- How would you get a detachable handler without allocating one function per instance?Keep the prototype method and bind at the boundary instead of in the class: pass `() => instance.handleClick()` at the single call site that needs a detached reference. That allocates per call site rather than per instance, and the prototype method stays shared, overridable, patchable, and reachable through `super`.
- Can a subclass override a base-class arrow field at all?Only by declaring its own field of the same name. Derived-class field initializers run after the base class's, so the derived initializer overwrites the own property. A subclass *method* of that name cannot win, because the own property always shadows the prototype, and `super.name()` cannot reach a base field either.
saying these in an interview costs you the question
- Says both forms end up on the prototype
- Thinks the arrow field is allocated once per class
- Claims class fields are non-enumerable like methods
- Believes a subclass method overrides a base arrow field
- Says bind on the field can retarget its this