Chapter 2: Context Is Everything
Timothy arrived at the library with a fresh cup of tea and a confused expression. He'd spent the morning trying to debug a simple JavaScript application, and something had broken his brain.
"Margaret, I need help," he said, pulling up his code. "I wrote what seems like straightforward code, but this keeps pointing to the wrong thing. Sometimes it's the object I expect, sometimes it's undefined, sometimes it's the global window object. It's driving me crazy."
Margaret looked at his code—a simple event handler attached to a button.
const user = {
name: "Alice",
greet: function() {
console.log("Hello, " + this.name);
}
};
user.greet(); // "Hello, Alice" - works!
const greetFunction = user.greet;
greetFunction(); // "Hello, undefined" - WHAT?!
She smiled knowingly. "Ah, the most famous source of JavaScript confusion. You've discovered the truth: this isn't what you think it is."
"But it's the same function!" Timothy protested. "Why does it behave differently?"
"Because," Margaret said, settling into her chair, "in JavaScript, this isn't determined by where the function is defined. It's determined by how the function is called. And that's a completely different mental model than Python."
Timothy groaned. "Already comparing to Python?"
"You need to understand the difference. In Python, self is explicit. You write it in every method signature. It's unambiguous. In JavaScript, this is implicit. It's determined at call time, not definition time. And that's where the confusion lives."
The Four Rules of this
Margaret pulled out her worn notebook and opened to a section marked "The JavaScript Mystery."
"Timothy, there are four distinct ways this can be determined when a function is called. Master these four rules, and this stops being mysterious."
Rule 1: Method Call
Margaret wrote:
const user = {
name: "Alice",
greet: function() {
console.log(this); // The object (user)
}
};
user.greet(); // this = user
"When you call a function as a method on an object—using dot notation—this refers to the object itself. This is intuitive. It's what most people expect."
Timothy nodded. "That makes sense."
"It does. Now the trouble."
Rule 2: Function Call
function greet() {
console.log(this);
}
greet(); // this = undefined (in strict mode) or window (in non-strict)
"When you call a function directly, not as a method on an object, this depends on whether you're in strict mode. In strict mode—which is modern JavaScript—this is undefined. In non-strict mode, it's the global object (window in browsers, global in Node.js). Either way, it's not what you want."
"Why would anyone do that?" Timothy asked.
"They wouldn't intentionally. But watch what happens when you extract a method from an object."
Margaret rewrote his original example:
const user = {
name: "Alice",
greet: function() {
console.log("Hello, " + this.name);
}
};
user.greet(); // "Hello, Alice" - Rule 1: method call, this = user
const greetFunction = user.greet; // Extract the function
greetFunction(); // "Hello, undefined" - Rule 2: function call, this = undefined
"See? You extract the function from the object, and now it's a plain function call. this becomes undefined. The function doesn't remember where it came from."
Rule 3: Constructor Call
function User(name) {
this.name = name;
console.log(this); // A brand new object
}
const alice = new User("Alice");
console.log(alice.name); // "Alice"
"When you call a function with the new keyword, JavaScript creates a brand new object and sets this to that object. The function then populates that object with properties. This is how you create objects in JavaScript."
Timothy leaned back. "Okay, that's different from Python. But I understand the pattern."
"Good. Now the fourth rule—this is where things get modern."
Rule 4: Explicit Binding with call, apply, and bind
function greet(greeting) {
console.log(greeting + ", " + this.name);
}
const user = { name: "Alice" };
greet.call(user, "Hello"); // "Hello, Alice"
greet.apply(user, ["Hello"]); // "Hello, Alice"
const boundGreet = greet.bind(user);
boundGreet("Hello"); // "Hello, Alice"
"With call and apply, you explicitly say: 'Call this function with this set to this object.' bind does the same thing but returns a new function with this permanently set."
Margaret closed her notebook. "Those are the four rules. Every time this appears in a function, one of these rules applies. There's no mystery—just patterns."
She sketched a simple decision tree:
How is the function called?
│
├─ With 'new'?
│ └─ Rule 3: Constructor Call
│ → this = newly created object
│
├─ With .call(), .apply(), or .bind()?
│ └─ Rule 4: Explicit Binding
│ → this = the object you specified
│
├─ As object.method()?
│ └─ Rule 1: Method Call
│ → this = the object (before the dot)
│
└─ Plain function()?
└─ Rule 2: Function Call
→ this = undefined (strict) or global (non-strict)
"When you're debugging this problems," Margaret said, "walk through this tree. Ask yourself: How was this function actually called? Follow the path, and you'll know what this should be."
The Problem: Losing Context
Timothy thought for a moment. "So the issue with my original code—extracting the function from the object—that's Rule 2 kicking in?"
"Exactly. And it happens all the time in real code. Watch."
Margaret pulled up a realistic example:
const button = document.querySelector('button');
const user = {
name: "Alice",
handleClick: function() {
console.log(this.name); // What is this?
}
};
button.addEventListener('click', user.handleClick);
// When clicked: console.log undefined
// addEventListener calls the function with this set to the button element,
// not the user object. So this.name tries to access button.name (undefined)
"The addEventListener method receives your function and calls it later. When it calls it, the standard behavior is to set this to the button element (the target of the event). So this.name tries to access button.name, which doesn't exist. It becomes undefined."
Timothy frowned. "How do you fix that?"
"Three ways. Pick the one that fits your situation."
Fix 1: Use an Arrow Function
const user = {
name: "Alice",
setupButton: function() {
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log(this.name); // this = user (captured from setupButton's scope)
});
}
};
user.setupButton();
"Arrow functions don't have their own this. They inherit this from the surrounding scope. Since the arrow function is defined inside setupButton, which is called as a method on the user object, this refers to user. Even though the button's event listener calls the arrow function, the arrow function looks outward and finds this from where it was defined."
Fix 2: Use bind
button.addEventListener('click', user.handleClick.bind(user));
// When clicked: console.log "Alice"
"You explicitly bind the function to the user object. Now, no matter how the function is called, this always refers to user."
Fix 3: Keep the Object Reference
button.addEventListener('click', () => {
user.handleClick(); // Call the method on the object
});
"Or you skip the whole this problem by calling the method directly on the object. Not the most elegant, but it works."
Margaret looked at Timothy. "Which approach appeals to you?"
"The arrow function seems cleanest."
"It is, for callbacks. But here's a critical warning: never use arrow functions as methods on an object. They don't have their own this."
She wrote:
const user = {
name: "Alice",
greet: () => {
console.log("Hello, " + this.name); // this is undefined or global, not user!
}
};
user.greet(); // "Hello, undefined"
"If you define an arrow function as a method, it inherits this from the surrounding scope—usually the global scope. Use regular functions for methods. Use arrow functions for callbacks."
Timothy nodded. "So arrow functions are for callbacks, regular functions are for methods."
"Exactly. It's about where the function is defined and what you need this to be."
When this Surprises You
Timothy pulled up another piece of code. "But here's where I got really confused. I have a callback inside a callback."
He also showed Margaret another example. "And I deconstructed a method from an object."
const user = {
name: "Alice",
greet: function() {
console.log("Hello, " + this.name);
}
};
const { greet } = user; // Destructuring extracts the function
greet(); // "Hello, undefined" - Rule 2 applies!
"This is the same as assigning it to a variable. Destructuring extracts the function, and now it's a Rule 2 function call."
Margaret nodded. "Modern syntax, same problem. Now let's look at your callback issue."
const user = {
name: "Alice",
fetchData: function() {
console.log("Before fetch, this =", this); // user object
fetch('/api/user').then(response => {
console.log("Inside then, this =", this); // Still user!
return response.json();
}).then(data => {
console.log("Inside second then, this =", this); // Still user!
this.data = data;
});
}
};
user.fetchData();
"This works, and I don't know why. The inner functions seem to know about this."
Margaret smiled. "Arrow functions. You're using arrow functions inside your Promise chains. Arrow functions capture this from their surrounding scope. So even though the Promise is calling the arrow function as a Rule 2 function call, the arrow function looks outward and says: 'What's the this in the scope where I was defined?' And it finds the user object."
"So arrow functions are magic?"
"Not magic. They're a solution to this exact problem. Before arrow functions existed, developers had to use bind or the var self = this pattern."
Margaret wrote:
const user = {
name: "Alice",
fetchData: function() {
var self = this; // Old pattern
fetch('/api/user').then(function(response) {
return response.json();
}).then(function(data) {
self.data = data; // Use the stored reference
});
}
};
"This is how people handled it before ES6. They saved a reference to this in a variable called self or that. Then they could access it in the nested function. It works, but it's ugly."
Timothy grimaced. "Yeah, I'm glad we have arrow functions."
"Everyone is. It's one of ES6's best additions for exactly this reason."
The Constructor Confusion
Timothy scrolled through his code. "I also have some constructor functions. Do I need to worry about this there?"
Margaret nodded. "Yes, but it's simpler. When you call a function with new, JavaScript creates a new object and sets this to that object."
function User(name, email) {
this.name = name;
this.email = email;
this.greet = function() {
console.log("Hello, " + this.name);
};
}
const alice = new User("Alice", "alice@example.com");
alice.greet(); // "Hello, Alice"
"Here, new User(...) creates a fresh object, sets this to that object, and then every property you assign goes onto that object. It works predictably because you're not extracting methods or passing callbacks—you're directly calling methods on the created object."
"What if you forget the new keyword?"
Margaret wrote:
const bob = User("Bob", "bob@example.com"); // Oops, forgot 'new'
console.log(bob); // undefined
console.log(window.name); // "Bob" - properties went on global object!
"Disaster. this becomes the global object, and you pollute the global scope. This is why modern JavaScript prefers classes, which enforce the new keyword."
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
console.log("Hello, " + this.name);
}
}
const alice = new User("Alice", "alice@example.com");
alice.greet(); // "Hello, Alice"
"Classes look different, and they use the same underlying mechanism of new and this binding. But they add important safeguards: classes cannot be called without new, and they handle inheritance differently than constructor functions. The syntax makes your intent clearer and prevents accidental mistakes."
Timothy paused. "But wait—if classes are safer, do class methods automatically keep their this binding? Like, if I extract a method from an instance?"
Margaret shook her head. "No. That's a common misconception. Class methods still aren't automatically bound. Watch:"
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log("Hello, " + this.name);
}
}
const alice = new User("Alice");
alice.greet(); // "Hello, Alice" - works!
const greet = alice.greet; // Extract the method
greet(); // TypeError: Cannot read property 'name' of undefined
"The class syntax doesn't magically bind methods. It just makes it obvious that you need new to create instances. The this rules still apply. If you extract the method, Rule 2 kicks in—it's a function call, and this is lost."
"So I still need bind or arrow functions?"
"Exactly. Classes don't solve the binding problem. They just make object creation safer. When you need a bound method, you use the same fixes: bind, arrow functions in callbacks, or call the method on the instance."
The Rule of Thumb
Margaret stood and walked to the window, looking out at the London streets.
"Timothy, this is confusing because it's determined at call time, not definition time. But if you remember the four rules—method call, function call, constructor call, and explicit binding—you can predict this in any situation."
She turned back. "And for modern JavaScript, there's a simpler rule of thumb:"
Use methods on objects for Rule 1 behavior. Call them as
object.method().Use arrow functions for callbacks. They inherit
thisfrom their surrounding scope, solving the "losing context" problem.Use
bindwhen you need explicit control. When you want to permanently setthison a function.Use classes for constructors. They're clearer and prevent the "forgot new" mistake.
"Follow these patterns, and you'll almost never have this problems."
Timothy made a note. "And if I do run into trouble?"
"You'll know the four rules, so you can debug it. But honestly, with arrow functions and classes, most modern JavaScript avoids the deep this problems. They're a relic of older code."
She returned to her chair. "Tomorrow, we'll look at something that might surprise you: how JavaScript prototypes work. It sounds like inheritance, but it's radically different from classes. And understanding prototypes means understanding how this works in a deeper way."
Timothy shook his head. "There's always another layer, isn't there?"
Margaret laughed. "Always, Timothy. Always."
Key Takeaways
thisis determined at call time, not definition time — The same function can have differentthisvalues depending on how it's called.Rule 1: Method Call — When you call a function as
object.method(),thisrefers to the object.Rule 2: Function Call — When you call a function directly (not on an object),
thisisundefined(in strict mode) or the global object (non-strict).Rule 3: Constructor Call — When you call a function with
new, JavaScript creates a new object and setsthisto that object.Rule 4: Explicit Binding — You can explicitly set
thisusingcall(),apply(), orbind().Arrow functions don't have their own
this— They inheritthisfrom the surrounding scope where they're defined.Extracting a method breaks context — When you assign
object.methodto a variable, it becomes a function call (Rule 2), andthisis lost.bindpermanently setsthis—function.bind(object)returns a new function withthisalways set to that object.Classes enforce constructor behavior — Using
classsyntax makes it clear that you neednew, preventing accidental mistakes.Modern JavaScript patterns avoid
thisconfusion — Use arrow functions for callbacks, regular functions (not arrow functions) as methods, and classes for constructors.
Discussion Questions
Why do you think JavaScript made
thisdetermined by call time instead of definition time (like Python'sself)?In what situations would you use
call()orapply()instead ofbind()?How does using arrow functions change the way you think about
thiscompared to regular functions?What's the difference between extracting
object.methodand using an arrow function that callsobject.method()?How does the
classsyntax make usingthissafer compared to manual constructor functions, even though class methods aren't automatically bound?
Share your this discoveries and debugging stories in the comments!
About This Series
The Secret Life of JavaScript reveals how JavaScript actually works—not how you wish it worked. Through the conversations of Timothy (a curious developer learning his first new language) and Margaret (a wise JavaScript expert) in a Victorian library in London, we explore the ideas beneath JavaScript's quirky syntax.
Each article stands alone but builds toward deeper understanding. Whether you're new to JavaScript or just curious about why it works the way it does, this series illuminates the path.
Coming next: "The Secret Life of Prototypes" — where inheritance isn't what you think it is, and objects delegate to other objects in ways that will blow your mind.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)