Object-Oriented Programming in JavaScript
This sequence builds JavaScript object-oriented ideas incrementally: we start from object literals, then add methods, explore call context for this, emulate encapsulation with closures, and finish with constructor functions and prototypes.
1) Start with an object literal and dynamic properties
We define an object with two fields and inspect existing and missing properties.
let o = {
n: "CSC 347",
count: -1
}
console.log(o)
console.log("o's name: " + o.n)
console.log("o's count: " + o.count)
console.log("o's next property: " + o.next)
o.next = 0
o["my next property"] = 1The key point is that JavaScript objects are open and extensible at runtime. Before adding behavior, we inspect the full set of properties.
2) Enumerate properties
A for...in loop walks all enumerable properties.
console.log("All of o's properties:")
for (p in o) {
console.log(p + " = " + o[p])
}Now we add behavior by storing a function in a property.
3) Add a method and observe this
next is added as a function value and uses this.count.
o.next = function() {
console.log(this)
return ++this.count;
}
console.log("Count = " + o.count + " and next " + o.next())
let fn = o.next
console.log("In method context = " + o.next())
console.log("In function context = " + fn())Method call context (o.next()) and plain function call context (fn()) differ. This motivates a design that does not expose mutable state directly.
4) Emulate encapsulation with closures
A factory function keeps count private and only exposes a next operation.
function createCounter(initial) {
let count = initial
let o = {
next: function() {
return ++count;
}
}
return o;
}
let o2 = createCounter(-1)
console.log("o2.count = " + o2.count)
console.log("o2.next() = " + o2.next())
console.log("o2.next() = " + o2.next())Since count is not a public property, clients cannot directly mutate it. Next we compare this style with constructor functions and new.
5) Constructor function usage and call context
Using new creates a fresh receiver object and binds it to this. Calling without new changes behavior significantly.
function Counter(initial) {
let count = initial
this.next = function() {
return ++count;
}
}
let o3 = new Counter(3)
console.log(o3.next())
let o4 = Counter(4)
console.log(o4)
let o5 = { Counter: Counter }
o5.Counter(4)
console.log(o5.next())This demonstrates why call site matters for constructor-style functions. The next issue is efficiency: each instance currently gets its own copy of next.
6) Shared behavior with prototypes
When methods are created in the constructor body, different instances do not share the same function object.
let o6 = new Counter(6)
let o7 = new Counter(7)
console.log("Same next? " + (o6.next == o7.next))To share behavior, move methods to the prototype.
7) Define methods on the prototype chain
Instances of Counter2 delegate method lookup to Counter2.prototype.
function Counter2(initial) {
this.count = initial
}
Counter2.prototype.next = function() {
return ++this.count;
}
let o8 = new Counter2(8)
let o9 = new Counter2(9)
console.log("o8.next() " + o8.next())
console.log("o9.next() " + o9.next())
console.log("Same next? " + (o8.next === o9.next))Now both objects share one next implementation. Because delegation is dynamic, extending the prototype updates behavior for all linked instances.
8) Extend prototype after instance creation
Adding reset later still affects existing objects through delegation.
Counter2.prototype.reset = function() {
this.count = -1;
}
o8.reset();
console.log("o8 reset count " + o8.count)This final step ties together JavaScript OOP mechanics: object literals, dynamic properties, closure-based encapsulation, constructor call context, and prototype-based method sharing.