Replacing a method on a class prototype is observed by instances that already exist, but not by instances constructed afterwards.
class C { m() { return "orig"; } }
const c = new C();
console.log(c.m()); // orig
(C.prototype as any).m = function () { return "replaced"; };
console.log(c.m()); // replaced ✓
console.log(new C().m()); // orig ✗ node: replaced
|
existing instance |
fresh instance |
| node 26.5.1 |
replaced |
replaced |
| perry |
replaced |
orig |
The asymmetry is the useful clue: the existing instance picks the replacement up, so the guard invalidation on the shared prototype works. It is construction that reinstalls the original surface — a fresh new C() appears to take the class's declaration-prototype vtable as recorded at class-registration time rather than consulting the (now mutated) prototype object.
A 200-iteration loop calling c.m() returns replaced, and a second replacement followed by another 200 iterations returns again, so the inline-cache path re-checks correctly for an existing receiver.
Confirmed pre-existing on main by A/B — rebuilding with #9169's runtime files reverted gives the identical orig. Found while auditing #9169, which fixes two adjacent per-instance [[Prototype]] cases and does not address this one.
Monkey-patching a class prototype after construction is common in test doubles and instrumentation libraries, and the failure is silent: the call succeeds and returns the stale implementation.
Replacing a method on a class prototype is observed by instances that already exist, but not by instances constructed afterwards.
replacedreplacedreplacedorigThe asymmetry is the useful clue: the existing instance picks the replacement up, so the guard invalidation on the shared prototype works. It is construction that reinstalls the original surface — a fresh
new C()appears to take the class's declaration-prototype vtable as recorded at class-registration time rather than consulting the (now mutated) prototype object.A 200-iteration loop calling
c.m()returnsreplaced, and a second replacement followed by another 200 iterations returnsagain, so the inline-cache path re-checks correctly for an existing receiver.Confirmed pre-existing on
mainby A/B — rebuilding with #9169's runtime files reverted gives the identicalorig. Found while auditing #9169, which fixes two adjacent per-instance[[Prototype]]cases and does not address this one.Monkey-patching a class prototype after construction is common in test doubles and instrumentation libraries, and the failure is silent: the call succeeds and returns the stale implementation.