Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

The this Keyword in JavaScript: Call-Site Rules, Arrow Functions, and Lost this

this is the most misunderstood feature in JavaScript โ€” not because itโ€™s complicated, but because it violates the intuition everything else builds. Scope and closures taught you that a functionโ€™s variables are determined by where itโ€™s written. this plays by the opposite rule: itโ€™s determined by how the function is called.

Same function, different call styles, different this. Once you accept that inversion and learn the four call-site rules (plus the arrow-function exception that deliberately breaks them), this becomes boringly predictable. This guide gets you there.


The Four Rules, In Priority Order

Given a function-style function (arrows come later), determine this by looking at the call site โ€” the line where the function is invoked:

Rule 1: Method call โ€” this is the object before the dot

const account = {
owner: "Priya",
describe() {
return `Account of ${this.owner}`;
},
};
account.describe(); // "Account of Priya" โ€” this === account

The rule is purely syntactic: X.f() sets this = X inside that call of f. This covers the vast majority of intuitive uses โ€” methods accessing their own objectโ€™s data.

Rule 2: Plain call โ€” this is undefined (strict mode)

const fn = account.describe; // extracting the function โ€” no dot at CALL time
fn(); // TypeError: Cannot read properties of undefined

Called without any object โ€” f() โ€” this is undefined in strict mode (which modules and classes put you in automatically; in legacy sloppy mode itโ€™s the global object, a historical mistake). Note what happened above: the same function behaved differently because the call site changed. The dot at definition time is irrelevant; only the dot at call time counts.

Rule 3: Explicit binding โ€” call, apply, bind

function describe() { return `Account of ${this.owner}`; }
const a = { owner: "Priya" }, b = { owner: "Arun" };
describe.call(a); // "Account of Priya" โ€” this set explicitly
describe.apply(b); // "Account of Arun" โ€” apply: args as an array
const bound = describe.bind(a);
bound(); // "Account of Priya" โ€” permanently bound, however called

call/apply invoke immediately with a chosen this; bind returns a new function with this locked forever (rebinding a bound function does nothing). bind is the pre-arrow fix for the โ€œlost thisโ€ problem below, and youโ€™ll still see it in class constructors: this.handleClick = this.handleClick.bind(this).

Rule 4: Constructor call โ€” this is the new object

function Account(owner) {
this.owner = owner; // this = the freshly created object
}
const acct = new Account("Priya");

new f() creates a fresh object, sets this to it, runs the body, and returns the object. In modern code youโ€™ll write this as a class, where the same rule applies inside constructor.

Priority when rules collide: new beats explicit bind, which beats method-dot, which beats plain call. In practice youโ€™ll rarely need the tiebreaker โ€” recognizing which single rule applies covers real code.


The Classic Bug: Losing this

Every JS developer hits this wall, usually via a callback:

const cart = {
items: [],
paise: 0,
addItem(item) {
this.items.push(item);
this.paise += item.paise;
},
};
// direct call: fine
cart.addItem({ name: "book", paise: 29900 });
// as a callback: broken
items.forEach(cart.addItem); // TypeError โ€” this is undefined inside
button.addEventListener("click", cart.addItem); // same disease
setTimeout(cart.addItem, 100); // same

Why: cart.addItem as an argument passes the bare function โ€” the extraction from Rule 2. When forEach/the event system/the timer later invokes it, the call site is a plain call. The cart. you typed was at pass time, not call time, and only call time counts.

The fixes, in modern preference order:

// 1. Wrap in an arrow โ€” the call site becomes cart.addItem(...), Rule 1 restored
items.forEach((item) => cart.addItem(item));
// 2. bind โ€” create a permanently-bound version
items.forEach(cart.addItem.bind(cart));

If you remember one diagnostic from this guide: โ€œcannot read properties of undefined (reading โ€˜xโ€™)โ€ inside a method you passed somewhere = lost this. Look at the call site, not the definition.


Arrow Functions: No this of Their Own

Arrows opt out of the whole system: an arrow function has no this binding โ€” this inside it is simply the this of the surrounding scope, resolved lexically like any closed-over variable. Call-site rules donโ€™t apply to arrows; call/bind canโ€™t change their this.

This makes arrows exactly right in one place and exactly wrong in another:

Right: callbacks inside methods โ€” the historical pain point, solved:

const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds += 1; // arrow: inherits this from start() โ†’ timer โœ“
}, 1000);
},
};

Pre-arrows, that callback was a function with its own (undefined) this, and code was littered with const self = this; workarounds and .bind(this) calls. Arrows made โ€œcallback that uses my objectโ€ just work โ€” this is the reason arrows exist beyond brevity.

Wrong: as methods themselves:

const account = {
owner: "Priya",
describe: () => `Account of ${this.owner}`, // BROKEN โ€” arrow's this is the
}; // module/global scope, not account
account.describe(); // "Account of undefined"

An arrow defined at an object literalโ€™s top level inherits the surrounding scopeโ€™s this โ€” which is not the object (object literals donโ€™t create a this). Same trap in class fields for methods that other code will call as methodsโ€ฆ though the same behavior is the fix for handlers:

class Toolbar {
label = "Save";
handleClick = () => { // class-field arrow: this locked to the instance
console.log(this.label); // works even as a detached event handler
};
}
button.addEventListener("click", new Toolbar().handleClick); // โœ“ no bind needed

The working ruleset that falls out:


this in the DOM and Other Environments

A few environment-specific notes that round out the picture:

button.addEventListener("click", function () {
this.classList.add("active"); // DOM sets this = the element (function-style only)
});
button.addEventListener("click", (e) => {
e.currentTarget.classList.add("active"); // arrow: use the event object instead
});

The DOM invokes function-style handlers with this set to the listening element โ€” old tutorials rely on it heavily. With arrows, that binding canโ€™t reach you; use e.currentTarget (the element the listener is attached to โ€” generally what you want over e.target, which is the innermost clicked element). Modern advice: prefer the event object regardless of function style; itโ€™s explicit and refactor-proof.

At the top level of an ES module, this is undefined. In Nodeโ€™s CommonJS modules itโ€™s module.exports (trivia; donโ€™t use it). And in class code, everything is strict mode โ€” no silent global-object fallback anywhere youโ€™ll write new code.


bind Beyond this: Partial Application

bind has a second, underused talent: pre-filling arguments. After the this value, any further arguments are locked in front of the call-time ones:

function log(level, source, message) {
console.log(`[${level}] ${source}: ${message}`);
}
const warn = log.bind(null, "WARN"); // level fixed
const dbWarn = log.bind(null, "WARN", "db"); // level + source fixed
warn("api", "slow response"); // [WARN] api: slow response
dbWarn("connection pool low"); // [WARN] db: connection pool low

This is partial application โ€” specializing a general function by fixing arguments โ€” and itโ€™s the same idea as closure-based factories wearing built-in syntax. The arrow equivalent (const warn = (src, msg) => log("WARN", src, msg)) is usually clearer in modern code, but bind-for-arguments appears in older codebases and libraries, so read it fluently. When this doesnโ€™t matter, the convention is passing null as the first argument, as above.

While weโ€™re on method borrowing: the explicit-binding rule enables genuinely useful tricks with array-like objects โ€” Array.prototype.slice.call(arguments) was the pre-spread idiom for converting arguments to an array, and youโ€™ll still meet Object.prototype.toString.call(x) as the most precise type probe in the language ("[object Date]", "[object Null]" โ€” distinguishing what typeof canโ€™t). Both are Rule 3 applied deliberately: run this method as if it belonged to that object.

Frequently Asked Questions

Why did JavaScript design this around call sites at all? Method reuse: one function can serve many objects. describe.call(anyAccount) and prototype methods shared by thousands of instances (next chapter) all rely on late-bound this โ€” the function borrows the identity of whoever calls it. The design predates its own worst ergonomics; arrows later patched the callback hole rather than changing the rules.

Is self = this still a thing? Only in legacy code. Reading it: self (or that) is a closure-carried snapshot of the outer this โ€” exactly what arrows now do implicitly. Writing it today would be a review comment.

Do I even need this if I prefer closures/factories? Substantially less than older code did โ€” factory functions with closure-private state avoid this entirely, and much modern React is this-free. But classes, the DOM, and countless libraries speak this fluently, so reading it remains non-optional even where you rarely write it.

How does TypeScript help? TS can type this per function, flags this usage in standalone functions, and its noImplicitThis catches many lost-this bugs at compile time โ€” one more example of TS value scaling with your understanding of the underlying JS behavior.

Quick way to debug a this problem? console.log(this) first line of the function, then find the call site and match it against the four rules. The bug is at the call site 95% of the time โ€” usually an extracted method or a function-callback that should be an arrow.

Does this exist in modulesโ€™ top level, getters, and event handler attributes? Module top level: undefined (covered above). Getters/setters: this is the object the property was accessed on โ€” Rule 1 applies, which is what makes get fullName() { return this.first + this.last } work. Inline handler attributes (onclick="..." in HTML): the element โ€” one more reason to prefer addEventListener, where the rules are the ones youโ€™ve learned.

Why does extracting methods work fine in Python but break in JS? Python binds methods to instances at access time (obj.method returns a bound method object); JS property access returns the bare function, binding only at call time. Neither is wrong โ€” JSโ€™s late binding is what enables borrowing and prototype sharing โ€” but itโ€™s why JS needs bind/arrows where Python doesnโ€™t. Knowing the difference inoculates you when switching languages.


The Decision Card

Everything in this guide compressed to the card youโ€™d keep at your desk:

Writing a... โ†’ use
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
method on an object/class โ†’ method shorthand: name() {}
callback needing enclosing this โ†’ arrow function
handler passed off the object โ†’ class-field arrow, or bind once
function with no this at all โ†’ arrow (brevity) or function (hoisting)
utility invoked on varied objects โ†’ function + call/apply by design
Debugging "this is undefined" โ†’ find the CALL SITE, apply the 4 rules
Debugging "this is the wrong thing" โ†’ extracted method or function-callback;
wrap in arrow or bind

Print-worthy or not, the compression is the point: four call-site rules, one lexical exception, two habitual fixes. this accounts for a wildly outsized share of JavaScript confusion given how little irreducible content it has โ€” the rules fit on an index card, and after a week of consciously applying them at call sites, they fit in reflex.

Self-Check

Predict each:

const obj = {
name: "A",
regular() { return this?.name; },
arrow: () => this?.name,
};
// 1
obj.regular();
// 2
const r = obj.regular; r();
// 3
obj.arrow();
// 4
obj.regular.call({ name: "B" });
// 5
const bound = obj.regular.bind({ name: "C" });
bound.call({ name: "D" });
// 6 โ€” inside a class method:
// setTimeout(function () { console.log(this); }, 0) vs
// setTimeout(() => console.log(this), 0)

Answers: (1) "A" โ€” method call. (2) undefined โ€” plain call, strict. (3) undefined โ€” arrow ignores the object, sees module this. (4) "B" โ€” explicit binding. (5) "C" โ€” bind is permanent; the later call loses. (6) the function logs undefined (its own unbound this); the arrow logs the instance (inherited lexically).

Six for six means this is done ambushing you. Next: Prototypes and Classes โ€” what new actually builds, the prototype chain underneath class syntax, and why โ€œJavaScript doesnโ€™t have real classesโ€ is both true and unimportant.