Methods (name() {}) are created once, when the class is evaluated, and stored on Class.prototype as writable, configurable, non-enumerable properties. Every instance reaches the same function object through the chain.
Fields (name = value;, or name;) are not on the prototype. Each field becomes an initializer function, stored in the constructor's [[Fields]] list. On every construction InitializeInstanceElements runs them with this set to the new instance and calls CreateDataPropertyOrThrow(this, name, value). So each instance gets an own, enumerable, writable, configurable data property, and a field without an initializer is created as undefined.
Consequences:
- Define, not set. A field never calls an inherited setter and isn't blocked by a read-only inherited property; it shadows them. A
this.x = ...assignment in the constructor does call the setter. - Timing. In a base class, fields are defined just before the constructor body runs. In a derived class, they're defined right after
super()returns, so a base constructor can't see the subclass's fields. Prototype methods, in contrast, are visible already. - Order and scope. Fields initialise top to bottom, and an initializer sees earlier fields only. Constructor parameters aren't in scope.
- Arrow-function fields capture the instance as
this, so they survive being passed as callbacks. The price: a new closure for every instance, and no shared prototype method for subclasses to override withsuper.method()or tests to spy on.
Rule of thumb: put behaviour in methods, put per-instance state in fields, and use an arrow field only when the method is routinely handed off as a callback.