Object Methods
Functions as object properties — how objects combine data and behaviour, and how this works.
1. Introduction
An object can store not just data (strings, numbers, booleans) but also functions. A function stored as a property is called a method. Methods let an object carry its own behaviour alongside its data — user.greet(), cart.total(), timer.start(). Inside a method, the keyword this refers to the object the method belongs to.
2. Theory
2.1 Defining methods
// Method shorthand (preferred)
const user = {
name: 'Alice',
age: 28,
greet() {
console.log(`Hi, I'm ${this.name}`);
},
isAdult() {
return this.age >= 18;
}
};
user.greet(); // "Hi, I'm Alice"
console.log(user.isAdult()); // true
// Older style — function expression as property
const user2 = {
name: 'Bob',
greet: function() {
console.log(`Hi, I'm ${this.name}`);
}
};
2.2 The this keyword
Inside a regular function used as a method, this refers to the object before the dot at call time.
const counter = {
count: 0,
increment() {
this.count++; // 'this' is the counter object
console.log(this.count);
},
reset() {
this.count = 0;
}
};
counter.increment(); // 1
counter.increment(); // 2
counter.reset();
counter.increment(); // 1
2.3 Arrow functions and this
Arrow functions do NOT have their own this. They inherit this from the surrounding scope. This is why you should NOT use arrow functions as methods.
const obj = {
name: 'Widget',
// Regular method — 'this' works correctly
goodMethod() {
console.log(this.name); // 'Widget'
},
// Arrow method — 'this' is NOT the object
badMethod: () => {
console.log(this.name); // undefined (or window.name in browser)
},
// Arrow function INSIDE a regular method — useful for callbacks
doAsync() {
setTimeout(() => {
console.log(this.name); // 'Widget' — inherits 'this' from doAsync
}, 100);
}
};
2.4 this in callbacks — the problem and fix
const timer = {
seconds: 0,
// Problem: regular function loses 'this' context in setInterval callback
startBroken() {
setInterval(function() {
this.seconds++; // 'this' is undefined (strict) or window (sloppy)
console.log(this.seconds); // NaN or error
}, 1000);
},
// Fix 1: arrow function (preferred)
startGood() {
setInterval(() => {
this.seconds++; // 'this' = timer object — correct
console.log(this.seconds);
}, 1000);
},
// Fix 2: save 'this' in a variable (older pattern)
startOld() {
const self = this;
setInterval(function() {
self.seconds++;
console.log(self.seconds);
}, 1000);
}
};
2.5 Getters and setters
const person = {
firstName: 'Alice',
lastName: 'Smith',
// Getter — computed property accessed like a regular property
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Setter — intercepts assignment
set fullName(value) {
const [first, ...rest] = value.split(' ');
this.firstName = first;
this.lastName = rest.join(' ');
}
};
console.log(person.fullName); // 'Alice Smith' — no ()
person.fullName = 'Bob Johnson'; // triggers setter
console.log(person.firstName); // 'Bob'
console.log(person.lastName); // 'Johnson'
2.6 Factory functions — creating many similar objects
// Factory function — returns a new object each time
function createUser(name, email, role = 'user') {
return {
name,
email,
role,
createdAt: new Date().toISOString(),
greet() {
return `Hello, ${this.name}!`;
},
isAdmin() {
return this.role === 'admin';
},
promote() {
return createUser(this.name, this.email, 'admin');
}
};
}
const alice = createUser('Alice', 'alice@example.com');
const bob = createUser('Bob', 'bob@example.com', 'admin');
console.log(alice.greet()); // "Hello, Alice!"
console.log(bob.isAdmin()); // true
console.log(alice.isAdmin()); // false
2.7 Object.keys/values/entries as utilities
const inventory = {
apples: 50,
bananas: 30,
oranges: 0,
grapes: 15
};
// Find out-of-stock items
const outOfStock = Object.keys(inventory).filter(item => inventory[item] === 0);
console.log(outOfStock); // ['oranges']
// Total inventory
const total = Object.values(inventory).reduce((sum, qty) => sum + qty, 0);
console.log(total); // 95
// Format for display
const display = Object.entries(inventory)
.map(([item, qty]) => `${item}: ${qty}`)
.join(', ');
console.log(display); // 'apples: 50, bananas: 30, oranges: 0, grapes: 15'
2.8 Object.fromEntries — entries back to object
// Convert array of pairs back to an object
const pairs = [['name', 'Alice'], ['age', 28], ['role', 'admin']];
const obj = Object.fromEntries(pairs);
console.log(obj); // { name: 'Alice', age: 28, role: 'admin' }
// Useful: transform an object's values
const prices = { shirt: 29, jeans: 59, jacket: 89 };
const doubled = Object.fromEntries(
Object.entries(prices).map(([key, val]) => [key, val * 2])
);
console.log(doubled); // { shirt: 58, jeans: 118, jacket: 178 }
// FormData to plain object
const form = document.querySelector('form');
const data = Object.fromEntries(new FormData(form));
console.log(data); // { username: '...', email: '...' }
3. Real World Example
// Shopping cart object with methods
function createCart() {
const items = [];
return {
get count() {
return items.reduce((sum, item) => sum + item.qty, 0);
},
get total() {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
},
add(product, qty = 1) {
const existing = items.find(i => i.id === product.id);
if (existing) {
existing.qty += qty;
} else {
items.push({ ...product, qty });
}
return this; // allow chaining: cart.add(p1).add(p2)
},
remove(productId) {
const idx = items.findIndex(i => i.id === productId);
if (idx >= 0) items.splice(idx, 1);
return this;
},
clear() {
items.length = 0;
return this;
},
summary() {
return items.map(i => `${i.name} x${i.qty} = $${(i.price * i.qty).toFixed(2)}`);
}
};
}
const cart = createCart();
cart.add({ id: 1, name: 'Shirt', price: 29 })
.add({ id: 2, name: 'Jeans', price: 59 })
.add({ id: 1, name: 'Shirt', price: 29 }, 2); // add 2 more shirts
console.log(cart.count); // 4
console.log(cart.total); // 29*3 + 59 = 146
console.log(cart.summary()); // ['Shirt x3 = $87.00', 'Jeans x1 = $59.00']
4. Code Example
<div id="timer-display">00:00</div>
<button id="start">Start</button>
<button id="pause">Pause</button>
<button id="reset">Reset</button>
<script>
const stopwatch = {
seconds: 0,
intervalId: null,
get display() {
const m = String(Math.floor(this.seconds / 60)).padStart(2, '0');
const s = String(this.seconds % 60).padStart(2, '0');
return `${m}:${s}`;
},
start() {
if (this.intervalId) return; // already running
this.intervalId = setInterval(() => {
this.seconds++;
document.querySelector('#timer-display').textContent = this.display;
}, 1000);
},
pause() {
clearInterval(this.intervalId);
this.intervalId = null;
},
reset() {
this.pause();
this.seconds = 0;
document.querySelector('#timer-display').textContent = this.display;
}
};
document.querySelector('#start').addEventListener('click', () => stopwatch.start());
document.querySelector('#pause').addEventListener('click', () => stopwatch.pause());
document.querySelector('#reset').addEventListener('click', () => stopwatch.reset());
</script>
5. Code Breakdown
Getter — display
The display getter computes a formatted time string from this.seconds on demand. It is accessed as stopwatch.display (no parentheses), yet it runs fresh every time — combining data and behaviour cleanly.
Arrow function in setInterval
The arrow callback inside setInterval inherits this from start(), which is the stopwatch object. Without the arrow function, this inside the callback would be undefined in strict mode.
Guard clause in start()
if (this.intervalId) return; prevents starting a second timer if start is called while already running — a classic guard clause pattern.
6. Common Mistakes
Mistake 1 — Arrow function as a method
const obj = {
value: 42,
getValue: () => this.value // 'this' is NOT obj — it's the outer scope
};
console.log(obj.getValue()); // undefined
// Fix — use method shorthand
const obj2 = {
value: 42,
getValue() { return this.value; } // 'this' = obj2
};
Mistake 2 — Losing this when passing a method as a callback
const player = {
name: 'Alice',
greet() { console.log(`I am ${this.name}`); }
};
// Bug — 'this' is lost when the method reference is detached
setTimeout(player.greet, 100); // 'I am undefined'
// Fix 1: wrap in arrow function
setTimeout(() => player.greet(), 100); // correct
// Fix 2: bind
setTimeout(player.greet.bind(player), 100); // correct
Mistake 3 — Mutating shared state in object methods
// If multiple objects share the same nested reference, mutations affect all
const template = { data: [], add(x) { this.data.push(x); } };
const a = { ...template };
const b = { ...template };
a.add(1);
console.log(b.data); // [1] — same array reference! Bug.
// Fix — ensure each instance gets its own array (use a factory function)
7. Best Practices
- Use method shorthand (
greet() { }) for methods — shorter and correctly setsthis. - Never use arrow functions as direct methods — they break
this. - Use arrow functions inside methods (as callbacks) — they correctly inherit the method's
this. - Use getters for computed properties that should look like regular properties to callers.
- Use factory functions when you need to create multiple similar objects.
- Return this from mutating methods to enable method chaining.
8. Practice Exercise
- Create a
bankAccountobject with propertiesownerandbalance, and methodsdeposit(amount),withdraw(amount)(reject if insufficient funds), andstatement()that logs the current balance. Test all methods. - Create a
calculatorobject with methodsadd,subtract,multiply,divide, andresult. Make each method returnthisso you can chain:calculator.add(5).multiply(3).subtract(2).result(). - Use
Object.fromEntriesandObject.entriesto write a functionmapValues(obj, fn)that appliesfnto every value and returns a new object.
9. Assignment
Build a "Task Manager" using an object-oriented approach.
- Create a
createTaskManager()factory function that returns an object with:addTask(title, priority),completeTask(id),deleteTask(id),filterByPriority(level),getStats()(returns totals, completed count, pending count), and acountgetter. - Wire up a UI: input for title, select for priority (high/medium/low), list of tasks with complete and delete buttons.
- Show stats (total/done/pending) that update automatically after every action.
- All task data lives inside the factory — UI only calls methods and reads from the returned object.
Deliverable: One HTML file.
10. Interview Questions
- What is a method in JavaScript?
A function stored as a property of an object. Called with object.method(), it has access to the object's other properties via this. - What does this refer to in a regular method?
The object the method was called on — specifically, the object before the dot at call time. If the method reference is detached (stored in a variable), this becomes undefined in strict mode. - Why shouldn't you use arrow functions as object methods?
Arrow functions don't have their own this — they inherit it from the lexical scope where they were defined. When used as methods, this will be the outer scope (often window or undefined), not the object. - What is a getter and when do you use it?
A getter is a method defined with the get keyword that is accessed as if it were a regular property (no parentheses). Use it for computed values that depend on other properties — like a fullName computed from firstName and lastName. - What is a factory function?
A regular function that creates and returns a new object. Each call produces an independent instance. It is a simple alternative to classes for creating multiple similar objects with shared methods.
11. Additional Resources
- MDN — Methods and this
- MDN — Getter / setter
- javascript.info — Object methods, "this"
- MDN — Object.fromEntries()