A method has no link to its object. this comes from the Reference Record produced by the expression left of the call parentheses. counter.increment evaluates to a reference with base counter and name "increment". If the call is made on that reference, EvaluateCall takes counter as this.
As soon as you read the function into a value, GetValue discards the reference: const callback = counter.increment stores, and setTimeout(counter.increment, 0) passes, just a function value.
The code that eventually calls it (setTimeout, forEach, a promise job, your own run(cb)) calls a plain function value, so there is no receiver. The callee gets undefined in strict code, so this.count throws; in sloppy code it gets globalThis, so you silently read or write globals, which is worse.
Fixes, in order of preference:
- Arrow wrapper:
run(() => counter.increment()). The call still hascounter.in front of it. bind:run(counter.increment.bind(counter)). A fixed receiver, and the right choice when you must pass a function and reuse it.- Class-field arrow:
increment = () => { ... }makes a per-instance function whosethisis the instance. thisArgparameter:array.map(fn, thisArg)and similar built-ins accept one.
Related: (0, obj.fn)(), (obj.fn || 0)() and (obj.fn = obj.fn)() also lose this. Each applies GetValue, so the call is on a value, not a reference. Plain parentheses, (obj.fn)(), keep the reference.