Week 8 Code Snippets

Author: Stefan Mitsch

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"] = 1

The 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.