In a JavaScript class body, declaring onClick = () => {} creates something different from declaring onClick() {}. Where does each one live, and what does that change for memory, overriding, and super calls?
answer
- field, not a method
- own property per instance
- nothing on the prototype
- own always beats inherited
- no super, no shared patch point
basics
~20 sAn arrow-function class field creates a separate function object on every instance, stored as an own property. A method is created once on the prototype and shared. The field version stays bound when detached, but costs memory per instance and cannot be overridden or reached through super.
solid answer
~40 s`onClick = () => {}` is a **field**, so a fresh closure is allocated for every instance and installed as an own property of that object. `onClick() {}` is a **method**, created once on `Class.prototype` and shared by all instances. The field version's appeal is that its `this` is fixed at construction, so passing `instance.onClick` somewhere as a callback still works. The costs are real: one function object per instance instead of one per class; nothing on the prototype, so `Class.prototype.onClick` is `undefined` and test doubles or instrumentation cannot patch it centrally; `super.onClick()` is unavailable from inside it; and because an own property always wins over the prototype, a subclass cannot override a parent's arrow field with a normal method — the parent's field shadows it.
code
javascript · 13 linesclass Widget {
onClick = () => this.name;
render() { return this.name; }
constructor(name) { this.name = name; }
}
const a = new Widget('a');
const b = new Widget('b');
console.log(a.onClick === b.onClick); // false — one closure each
console.log(a.render === b.render); // true — shared on prototype
console.log('onClick' in Widget.prototype); // false
console.log(Object.keys(a)); // [ 'onClick', 'name' ]go deeper
Recognise both forms and know the arrow field is created for each object while the method is written once for the class, so instance.handler can be passed around as a callback safely.
Explain the placement precisely — own property versus prototype entry — and derive the consequences yourself: identity differs across instances, Class.prototype has no such key, and Object.keys on the instance now lists a function.
Show the failure modes you have hit: a subclass override that never runs, a prototype spy that never fires, instrumentation that misses the member, and per-instance allocation in a large collection.
Set the house rule and justify it — which members are allowed to leave the prototype chain, what that costs in extensibility and observability for consumers of your classes, and how you keep bound callbacks from becoming un-overridable API.
## The same name, two very different things ```js class Widget { arrowVersion = () => this.id; // field: one closure per instance methodVersion() { return this.id; } // method: one function on the prototype constructor(id) { this.id = id; } } const a = new Widget(1), b = new Widget(2); a.arrowVersion === b.arrowVersion; // false a.methodVersion === b.methodVersion; // true 'arrowVersion' in Widget.prototype; // false ``` The arrow form is not a method at all. It is a field whose initializer happens to evaluate to a function, so it obeys every field rule: created during construction, in source order, as an own writable/enumerable/configurable property. ## Why anyone writes the arrow form A prototype method is just a function sitting on an object. Detach it — `const f = instance.methodVersion; f()` — and the call has no receiver, so `this` is `undefined` in the class body's strict code and the call throws. The arrow field never has that problem, because an arrow captures its surrounding `this` at creation time and the surrounding scope during field initialization is the instance under construction. Passing `instance.arrowVersion` to any callback-taking API therefore works with no wrapper. That is the whole benefit, and it is a genuine one. The alternatives are binding once in the constructor (`this.onClick = this.onClick.bind(this)`, which produces the same own-property shape and the same costs) or wrapping at the call site (`() => instance.onClick()`, which keeps the prototype method intact). ## Cost one: memory and allocation Every instance allocates its own function object plus the closure environment behind it. For a handful of long-lived objects that is noise. For a list of a hundred thousand rows, each with three arrow-field handlers, it is three hundred thousand function objects that a shared prototype method would have made zero of. This is the routine follow-up to the arrow-field question, and the honest answer is that it usually does not matter and occasionally matters a lot — measure before rewriting. ## Cost two: nothing is on the prototype ```js Widget.prototype.arrowVersion; // undefined ``` Several common techniques quietly depend on the prototype entry existing. Instrumentation that wraps `Class.prototype.method` to add logging or timing does nothing for an arrow field. A spy installed on the prototype in a test is never consulted, because the instance's own property is found first. Anything that enumerates a class's public behaviour by walking `Object.getOwnPropertyNames(Class.prototype)` misses it entirely — while `Object.keys(instance)` now returns your handler alongside the data, which is untidy for logging and serialization. ## Cost three: overriding and super Property lookup finds own properties before prototype properties. So if a parent declares an arrow field, a subclass's normal method with the same name is never reached: ```js class Base { greet = () => 'base'; } class Child extends Base { greet() { return 'child'; } } new Child().greet(); // 'base' — the own field shadows the prototype method ``` To override, the subclass must also declare a field, and now the parent's version is unreachable: `super.greet` looks on the parent *prototype*, where nothing was ever installed. Inside an arrow field you also have no `super` method access to fall back on. In other words, choosing the field form opts that member out of the inheritance mechanism. There is a related ordering hazard: because fields initialize in source order, an arrow field declared before another field can be *called* later and see the fully built object, but if you invoke it during construction the fields declared after it are still undefined. ## How to decide Use a prototype method by default: it is shared, overridable, reachable through `super`, patchable, and invisible to enumeration. Reach for the arrow field when the member is genuinely a **detached callback** you hand to another system and it will never need to be overridden — and know that you have traded a per-class function for a per-instance one, and taken that name out of the prototype chain.
- How much does the per-instance allocation actually cost, and when would you care?Each instance allocates one function object plus its closure environment per arrow field — small individually, but it scales linearly with instance count while a prototype method costs nothing extra. It is irrelevant for tens or hundreds of objects and can be worth removing for very large collections of short-lived objects. Measure heap and allocation rate before converting; do not rewrite on principle.
- If a member must both stay bound and remain overridable, what do you do?Keep the real behaviour as a prototype method so subclasses can override it and `super` still works, and expose the bound form separately — either wrap at the call site with `() => instance.method()`, or keep a thin arrow field that just delegates to `this.method()`. The delegation form preserves dynamic dispatch: the arrow looks the method up at call time, so an override is honoured.
- Why does a test spy installed on Class.prototype not intercept an arrow-field handler?Because the instance carries its own property with that name, and property lookup finds own properties before ever consulting the prototype. The prototype entry does not even exist for an arrow field. To double it you must replace the property on each instance, or restructure the class so the behaviour lives in a prototype method that the field merely delegates to.
saying these in an interview costs you the question
- Says an arrow class field lives on the prototype
- Claims arrow fields are always the better default
- Thinks a subclass method can override a parent's arrow field
- Assumes super works from inside an arrow class field
- Believes the per-instance function is free because functions are shared