`this`, `self`, `super` and Base Access
Learning Objectives
Use this/self to refer to the current object, and base-access syntax in each language.
Build fluent APIs with method chaining.
Explain JavaScript this binding and why arrow functions help.
Avoid letting this escape before construction completes.
Prerequisites: Modules 5, 7.
Intuition and Definition
Inside a member function, the current object is available as this (C++ pointer; Java/C#/TS reference) or self (Python, an explicit first parameter). Base-class members are reached with:
this (pointer), *this super.method(), super(...) in constructors base.Method(), : base(...) super().method() (follows the MRO)
this (depends on how the function is called!) super.method(), super(...)
Why It Matters
this makes fluent APIs possible and resolves name clashes, but misuse causes some of the nastiest bugs: callbacks with the wrong this in JavaScript, and objects registered with other threads or observers before they are fully constructed.
Syntax and Language Notes
class Request {
std::string url_;
public:
Request& url(std::string u) { url_ = std::move(u); return *this; } // chainable
};class Child(Base):
def __init__(self, x):
super().__init__() # call the next class in the MRO
self.x = xclass Button {
label = "OK";
handleBad() { return this.label; } // 'this' depends on the call site
handleGood = () => this.label; // arrow function captures 'this' of the instance
}Complete Example
JavaScript/TypeScript this binding — the same method behaves differently when detached:
Python super() follows the method resolution order, so each class in a diamond runs once:
#include <iostream>
#include <string>
#include <vector>
class Message {
std::string to_, subject_;
std::vector<std::string> lines_;
public:
Message& to(std::string t) { to_ = std::move(t); return *this; }
Message& subject(std::string s) { subject_ = std::move(s); return *this; }
Message& line(std::string l) { lines_.push_back(std::move(l)); return *this; }
void print() const {
std::cout << "To: " << to_ << "\nSubject: " << subject_ << "\n";
for (const auto& l : lines_) std::cout << " " << l << "\n";
}
};
class Base {
public:
virtual ~Base() = default;
virtual std::string greet() const { return "hello from Base"; }
};
class Derived : public Base {
std::string name_;
public:
explicit Derived(std::string name) : name_(std::move(name)) {}
std::string greet() const override {
return Base::greet() + " and " + this->name_; // base access + explicit this
}
};
int main() {
Message().to("service@example.com").subject("Inspection").line("Brake pads at 3 mm").line("Book a slot").print();
Derived d("Derived");
std::cout << d.greet() << "\n";
}To: service@example.com
Subject: Inspection
Brake pads at 3 mm
Book a slot
hello from Base and Derivedclass Counter {
count = 0;
incrementMethod() { this.count++; return this.count; }
incrementArrow = () => { this.count++; return this.count; };
}
const c = new Counter();
const detachedMethod = c.incrementMethod;
const detachedArrow = c.incrementArrow;
try {
detachedMethod(); // 'this' is undefined in strict mode
} catch (e) {
console.log("method lost its this:", (e as Error).message);
}
console.log("arrow keeps its this:", detachedArrow());
console.log("bound method:", c.incrementMethod.bind(c)());method lost its this: Cannot read properties of undefined (reading 'count')
arrow keeps its this: 1
bound method: 2class Base:
def setup(self):
print("Base.setup")
class Logging(Base):
def setup(self):
print("Logging.setup")
super().setup()
class Metrics(Base):
def setup(self):
print("Metrics.setup")
super().setup()
class Service(Logging, Metrics):
def setup(self):
print("Service.setup")
super().setup()
Service().setup()
print([cls.__name__ for cls in Service.__mro__])Service.setup
Logging.setup
Metrics.setup
Base.setup
['Service', 'Logging', 'Metrics', 'Base', 'object']Step-by-Step Walkthrough
Each Message setter returns *this by reference, so calls chain on the same temporary object, which lives until the end of the full expression — safely through print().
Base::greet() calls the base implementation explicitly, bypassing virtual dispatch.
In TypeScript, c.incrementMethod read without calling it loses its receiver; calling it plain gives this === undefined. The arrow-function field captured this when the object was created.
Python's super() does not mean "my parent" — it means "the next class in the MRO", so Logging.setup continues with Metrics.setup, and Base.setup runs exactly once.
Common Mistakes
**Returning *this by value from chain methods** — each call then operates on a copy.
Keeping a reference to a temporary returned by a fluent chain (auto& m = Message().to("x"); dangles at the semicolon).
this escape: registering this with an observer list or thread inside the constructor; the observer may call back before construction finishes (or before derived parts exist).
Passing methods as callbacks in JS without binding. Hard-coding the parent in Python (Base.setup(self)) in cooperative multiple inheritance — breaks the MRO chain.
Trade-offs and Alternatives
Fluent interfaces read nicely but make debugging and partial failures harder; a Builder (Module 30) with a final build() that validates is often clearer. Instead of registering this in constructors, use a factory that constructs first, then registers.
Real-World Use
Fluent APIs: query builders (db.select().from().where()), HTTP clients, test assertion libraries (expect(x).toBe(3)). JavaScript UI frameworks rely heavily on arrow functions to keep this bound in event handlers.
Practice
Q1. (Easy) What does return *this; return from Message& to(...)?
Answer: A reference to the same object, enabling chaining.
Q2. (Medium) Why is auto& m = Message().to("x"); dangerous?
Answer: Message() is a temporary destroyed at the end of the full expression; m then refers to a destroyed object.
Q3. (Advanced) Show a safe way to register an object as an observer without this escaping during construction.
#include <iostream>
#include <memory>
#include <vector>
struct Listener { virtual void onEvent(int e) = 0; virtual ~Listener() = default; };
struct Bus {
std::vector<std::weak_ptr<Listener>> ls;
void subscribe(const std::shared_ptr<Listener>& l) { ls.push_back(l); }
void publish(int e) { for (auto& w : ls) if (auto l = w.lock()) l->onEvent(e); }
};
class Display : public Listener {
explicit Display(int id) : id_(id) {} // private: construct only through create()
int id_;
public:
static std::shared_ptr<Display> create(Bus& bus, int id) {
std::shared_ptr<Display> d(new Display(id)); // fully constructed first…
bus.subscribe(d); // …then registered
return d;
}
void onEvent(int e) override { std::cout << "display " << id_ << " got event " << e << "\n"; }
};
int main() {
Bus bus;
auto d = Display::create(bus, 1);
bus.publish(42);
}display 1 got event 42Interview Question
Q1. "Why does a class method lose this when passed as a callback in JavaScript, and what are three fixes?"
Answer: In JavaScript, this is determined by *how a function is called*, not where it was defined. obj.method() sets this = obj, but const f = obj.method; f() calls it with this undefined (strict mode). Fixes: (1) bind it (obj.method.bind(obj)), (2) define it as an arrow-function field, which captures the instance's this, (3) wrap the call (() => obj.method()). Arrow fields cost one function per instance; prototype methods are shared.
Quick Quiz
Q1. What is Python's self?
Answer: The explicit first parameter of instance methods — the object the method was called on.
Q2. C++ syntax to call the base class's version of f?
Answer: Base::f().
Q3. What does Python's super() call?
Answer: The next class in the method resolution order (MRO), not necessarily the direct parent.
Summary
this/self refer to the current object; base access calls the parent implementation explicitly.
Return *this by reference for fluent chains; mind temporaries.
JavaScript this depends on the call site — bind or use arrow functions.
Don't let this escape during construction.
Related: Module 7 (inheritance), Module 30 (Builder), Module 41 (Observer), Module 81 (TypeScript/JS OOP).