Up to now your objects have been passive bundles of data — a name here, a score there — while the code that works on that data lived somewhere else, usually in a scattered pile of functions. That split holds for tiny scripts, but it breaks down fast: you end up passing the same object into a dozen functions, and nothing on the object tells you which functions were ever meant to touch it.
A method closes that gap. It is a function stored as a property, so the data and the behaviour that shapes it travel together. Instead of greet(user) you write user.greet() — the data knows what to do with itself. The one new idea a method adds is a special name, this, which lets the function refer to the very object it was called on.
flowchart LR D["data<br/>balance = 100"] --> O["object"] F["deposit(amount) method"] --> O O -->|"deposit(50)"| U["balance becomes 150"] style O fill:#e0900b,color:#fff style U fill:#1e7f3d,color:#fff
Methods and this
You can put a function on an object the same way you assign any value, but modern JavaScript also offers tidy method shorthand, which you should reach for by default:
const account = {
balance: 100,
deposit(amount) { // shorthand for deposit: function(amount)
this.balance += amount; // 'this' is the object the call happened on
return this.balance;
}
};
The word this is how a method refers to the object it was invoked on. When you write account.deposit(50), the thing before the dot — account — becomes this for the duration of the call, so this.balance means account.balance. Read the call aloud as 'account, run your deposit', and you have read this correctly.
const account = {
owner: "Ada",
balance: 100,
describe() {
return `${this.owner} has $${this.balance}`;
},
deposit(amount) {
this.balance += amount;
return this.balance;
}
};
console.log(account.describe());
console.log(account.deposit(25));
console.log(account.describe());
this is decided by the call, not the definition
This is the single most important fact about this, and the one beginners get wrong: this is not a permanent property of the method, fixed when it was written. It is chosen fresh on every call, based entirely on how the function is invoked. The definition of deposit never says which object this will be — it only promises 'whatever object I am called on, update its balance'.
So a.deposit(5) sets this to a, and b.deposit(5) sets this to b, even though both calls run the exact same function body. There is no lasting bond between a method and the object where it happens to live. The object before the dot, at the moment of the call, is the whole story.
flowchart TD M["describe() method"] -->|"a.describe()"| A["this = a"] M -->|"b.describe()"| B["this = b"] style A fill:#1e7f3d,color:#fff style B fill:#3776ab,color:#fff
function describe() {
return `${this.label}: ${this.value}`;
}
const a = { label: "A", value: 1, describe };
const b = { label: "B", value: 2, describe };
console.log(a.describe());
console.log(b.describe());
Arrow functions don't have their own this
There is a second kind of function in JavaScript that behaves very differently here. An arrow function does not get its own this at all — instead it inherits this from the surrounding code, wherever it was defined. Write const f = () => this.x inside a method and this still means the method's receiver, because the arrow reached outward and borrowed it.
That sounds exotic until you see why it exists. Callbacks and timers call your function as a bare f(), with no object before the dot — which, for a regular function, throws this away. An arrow ignores that call-site rule and keeps the this it captured, which is exactly what you want when a method hands a callback to something else. You will use this fact to repair a broken timer below.
flowchart TD R["obj.method() regular fn"] -->|"this = obj"| CR["from the call site"] AR["arrow inside method"] -->|"this = obj"| CA["borrowed from outer scope"] style CR fill:#e0900b,color:#fff style CA fill:#1e7f3d,color:#fff
const counter = {
count: 0,
bump() { this.count += 1; return this.count; }
};
// An arrow keeps `this` from the surrounding scope, so the inner call keeps its receiver.
const safe = () => counter.bump();
console.log(safe());
console.log(safe());
// .bind fastens the receiver on permanently, no matter how the function is later called.
const bound = counter.bump.bind(counter);
console.log(bound());
What does this print? The object before the dot is p, so inside label that is this.
const p = {
name: "Kit",
label() { return `[${this.name}]`; }
};
console.log(p.label());
pis before the dot, sothisispfor the call.this.nameis "Kit", wrapped in square brackets.
Write makeWallet() that returns an object with balance starting at 0 and two methods: credit(amount) adds amount to the balance and returns it; spend(amount) subtracts amount and returns it. Use this inside the methods to read and update balance.
function makeWallet() {
return {
balance: 0,
// add credit(amount) and spend(amount) using this
};
}
Inside each method, read and update this.balance.
credit adds: this.balance += amount; spend subtracts: this.balance -= amount.
End each method with
return this.balance;.
function makeWallet() {
return {
balance: 0,
credit(amount) { this.balance += amount; return this.balance; },
spend(amount) { this.balance -= amount; return this.balance; }
};
}
runLater grabs the tick method off counter, then calls the bare function — so tick runs with no receiver and this is lost. Keep makeCounter, but change the one line that detaches tick so it still runs with counter as its receiver (use .bind, or call it through counter directly).
function makeCounter() {
return {
count: 0,
tick() { this.count += 1; }
};
}
function runLater(counter) {
const fn = counter.tick; // detached from counter
fn(); // runs with no receiver, so this is lost
return counter.count;
}
fn()has no object before the dot, sothisis notcounter.Glue the receiver back on with
counter.tick.bind(counter).An arrow wrapper,
() => counter.tick(), would preservethistoo.
function makeCounter() {
return {
count: 0,
tick() { this.count += 1; }
};
}
function runLater(counter) {
const fn = counter.tick.bind(counter); // bind glues the receiver on
fn();
return counter.count;
}
Methods can call each other through this. Add total() to the cart object — it returns the sum of every item's price — and withTax(rate), which returns this.total() multiplied by (1 + rate). withTax MUST call this.total() rather than re-adding the prices; that is the skill being tested.
const cart = {
items: [{ price: 10 }, { price: 5 }, { price: 20 }],
// add total() and withTax(rate); withTax calls this.total()
};
total()should sumthis.items—reduceor a loop both work.withTaxmust callthis.total(), not re-readthis.items.Because the methods use
this, copying them onto another object still works.
const cart = {
items: [{ price: 10 }, { price: 5 }, { price: 20 }],
total() {
return this.items.reduce((sum, it) => sum + it.price, 0);
},
withTax(rate) {
return this.total() * (1 + rate);
}
};
An object q has who() { return this; }. When you call q.who(), what sets the value of this inside the method?
this is chosen at the call site, not the definition. In q.who() the receiver before the dot is q, so this is q for that call — regardless of where who was originally written.
Recap
- A method is a function stored on an object; reach for method shorthand
name(args) { ... }. - Inside a method,
thisis the object the call happened on — the receiver before the dot. thisis decided per call, by how the function is invoked, never fixed at definition.a.f()andb.f()run the same code with differentthis.- Arrow functions have no
thisof their own — they borrow it from the surrounding scope, which is why they fix callback bugs. - Detaching a method (storing it, passing it to a callback) loses
this. Repair it with an arrow wrapper or.bind(receiver).
Next you'll turn these one-off objects into reusable blueprints with constructor functions and the prototype chain — the machinery this was built to serve.