CSC447

Concepts of Programming Languages

JavaScript OOP

Instructor: James Riely

Video

Open on youtube

JavaScript OOP

  • class declarations are not primitive
  • Delegation/prototype based inheritance
    • like Self
    • runtime structures resemble vtables

Object Literals

  • Object literals have properties (name, age, addr)

var empty = {}; /* Object literal with no contents */ 
var person = {  /* Object literal with three properties */
  name: "Alice",
  age: 50,
  addr: "243 S Wabash Ave"
};
          

> [ person.name, person.name.length ]
[ 'Alice', 5 ]
          

> person.age = person.age + 1
51
          

> person.occupation
undefined                /* huh???? */
          

> person.occupation.length
Uncaught TypeError: Cannot read property 'length' of undefined
          

Dynamically Typed


function f (x) {
  console.log ("1: " + x);
  console.log ("2: " + x.data);
  console.log ("3: " + x.data.length);
}
          

> f ({ data: [11,21,31] });
1: [object Object]
2: 11,21,31
3: 3
          

> f ("pizza");
1: pizza
2: undefined
Uncaught TypeError: Cannot read property 'length' of undefined
          

Dynamically Typed


function f (x) {
  console.log ("1: " + (x / 2));
  console.log ("2: " + (x[0]));
}
          

> f ([11,21,31]);
1: NaN
2: 11
          

> f (8);
1: 4
2: undefined
          

> f (undefined);
1: NaN
Uncaught TypeError: Cannot read property '0' of undefined
          

Properties

  • Object is map from strings to values
    • Dot notation allowed when string has no spaces
  • Can add new properties, iterate through them

var person = { name: "Alice", age: 50, addr: "243 S Wabash Ave" };
          

person.occupation = "Developer";
          

person["happy place"] = "Home";
          

> for (p in person) { console.log (p + ": " + person[p]); }
name: Alice
age: 50
addr: 243 S Wabash Ave
occupation: Developer
happy place: Home
          

> JSON.stringify(person)
'{"name":"Alice","age":50,"addr":"243 S Wabash Ave","occupation":"Developer","happy place":"Home"}'
          

Function Properties


var counter1 = { n: 0, get: function () { return this.n++; } };
var counter2 = { n: 0, get () { return this.n++; } };
          

> [counter1.get(), counter2.get()]
[0, 0]
          

> [counter1.get(), counter2.get()]
[1, 1]
          

> [counter1.get, counter2.get]
[ [Function: get], [Function: get] ]
          

> [counter1.get.toString(), counter2.get.toString()]
[ 'function () { return this.n++; }', 'get () { return this.n++; }' ]
          

This required to access Properties


var counter1 = { n: 0, get: function () { return this.n++; } };
var counter3 = { n: 0, get: function () { return n++; } };
          

> counter1.get()
0
          

> counter3.get()
Uncaught ReferenceError: n is not defined
    at Object.get (repl:1:43)
          

No private properties


var counter1 = { n: 0, get: function () { return this.n++; } };
          

> counter1.get()
0
          

> counter1.n = -2000
          

> counter1.get()
-2000
          

Encapsulation Using closures


function createCounter4 () {
  var n = 0;
  return {
    get: function () { return n++; } 
  };
};
var [c4a,c4b] = [createCounter4(), createCounter4()]
          

> c4a.get()
0
          

> [c4a,c4b].map (x => x.get())
[ 1, 0 ]
          

> [c4a,c4b].map (x => x.get())
[ 2, 1 ]
          
  • createCounter returns an object literal with closure for get

closure variables are not fields


function createCounter5 () {
  var n = 0;
  return {
    get: function () { return this.n++; } 
    huh: function () { console.log ("this.n=" + this.n); }
  };
};
var c5 = createCounter5 ();
          

> c5.get ();
NaN          

> c5.huh ();
this.n=undefined
          

Closure versus Object

  • Can also represent counter as closure

function createCounter6 () {
  var n = 0;
  return function () { return n++; }; 
}
var [c6a,c6b] = [createCounter6(), createCounter6()]
          

> c6a()
0
          

> [c6a,c6b].map (x => x())
[ 1, 0 ]
          

> [c6a,c6b].map (x => x())
[ 2, 1 ]
          

binding this

  • JS binds this based on calling context. See here
    • o.m() is method context, this===o
    • f() is function context, this===global/window
    • new C() is constructor context, this is new object

var o = { getThis () { return this; } };
          

o.getThis() === o;     // true
          

var fThis = o.getThis; // save method as function 
          

fThis() === o          // false --- this=o not closed above
          

fThis() === global     // true (in Node.js)
fThis() === window     // true (in Browswer)
          

Javascript versus Scala


var oJS = { n: -1, add (i) { this.n += i; return this.n; }};
var fAdd = oJS.add 
fAdd(1) // NaN --- closure does not bind this=oJS
          

object oScala { var n = -1; def add(i:Int) = { this.n += i; this.n } }
var fAdd = oScala.add
fAdd(1) // 0 --- closure binds this=oScala
          
  • JS methods are functions
    • Methods are just properties that hold closures
    • this is not bound in the closure
    • Closure is returned when accessing the property
  • Scala Methods are not Functions
    • this is closed when converting method to function

Trouble


var o = {
  v : 5,
  add : function (xs) {
    return xs.map(function(x){ return this.v + x; });
  }
}
o.add([10,20,30]) // [ NaN, NaN, NaN ]
          

Old Hack:


var o = {
  v : 5,
  add : function (xs) {
    var me = this;
    return xs.map(function(x){ return me.v + x; });
  }
}
o.add([10,20,30]) // [ 15, 25, 35 ]
            

Trouble

ES5. Sad.


var o = {
  v : 5,
  add : function (xs) {
    return xs.map(function(x){ return this.v + x; }.bind(this));
  }
}
o.add([10,20,30]) // [ 15, 25, 35 ]
          

ES6. The right way:


var o = {
  v : 5,
  add : function (xs) {
    return xs.map(x => this.v + x);
  }
}
o.add([10,20,30]) // [ 15, 25, 35 ]
            

Fat arrow and this

Fat arrow treats this like a regular variable

this bound in closure rather than when called


var y = {getThese: function(xs){ return xs.map(x => this)}};
var z = {getThese: function(xs){ return xs.map(function(x){ return this; })}};
            

y.getThese([10,20,30])[0] === y      // true
z.getThese([10,20,30])[0] === global // true (in Node.js)
z.getThese([10,20,30])[0] === window // true (in Browswer)
            

Creating objects: Function Context

  • Create an object with a function
  • Call with function context
  • Function builds and returns object literal

function createCounter () {
  return {
    n: -1,
    next:  function () { return ++this.n; },
    reset: function () { this.n = -1;      }
  };
}
var c1 = createCounter (); // Function context 
var c2 = createCounter (); 
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 0, 1, 2, undefined, 0, 1 ]
> [c2.next(), c2.next(), c2.next(), c2.reset(), c2.next(), c2.next()]
[ 0, 1, 2, undefined, 0, 1 ]
          

Creating objects: Constructor Context

  • Call with constructor context
  • Object created with new
  • New object implicitly passed to function as this

function Counter () {
  this.n = -1;
  this.next = function () { return ++this.n; },
  this.reset = function () { this.n = -1; }
}
var c1 = new Counter (); // Constructor context
var c2 = new Counter (); 
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 0, 1, 2, undefined, 0, 1 ]
> [c2.next(), c2.next(), c2.next(), c2.reset(), c2.next(), c2.next()]
[ 0, 1, 2, undefined, 0, 1 ]
          

Sharing Properties

  • JS functions are objects, so can have properties
  • Define properties on prototype object of function

function Counter () { this.n = -1; }
Counter.prototype.next  = function () { return ++this.n; }
Counter.prototype.reset = function () { this.n = -1;     }

var c1 = new Counter ();
var c2 = new Counter ();
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 0, 1, 2, undefined, 0, 1 ]
> [c2.next(), c2.next(), c2.next(), c2.reset(), c2.next(), c2.next()]
[ 0, 1, 2, undefined, 0, 1 ]
          

Set Prototype Property

  • c1 and c2 share Counter.prototype
  • Counter.prototype resembles vtable for a class

function Counter () { this.n = -1; }
Counter.prototype.next  = function () { return ++this.n; }
Counter.prototype.reset = function () { this.n = -1;     }

var c1 = new Counter ();
var c2 = new Counter ();
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 0, 1, 2, undefined, 0, 1 ]
> [c2.next(), c2.next(), c2.next(), c2.reset(), c2.next(), c2.next()]
[ 0, 1, 2, undefined, 0, 1 ]
          

Very Dynamic

  • Update or add methods at runtime!

function Counter () { this.n = -1; }
Counter.prototype.next  = function () { return this.n += 1; }
Counter.prototype.reset = function () { this.n = -1;     }

var c1 = new Counter ();
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 0, 1, 2, undefined, 0, 1 ]
          

> Counter.prototype.next  = function () { return this.n += 2; }
> Counter.prototype.reset = function () { this.n = -2;     }
          

> [c1.next(), c1.next(), c1.next(), c1.reset(), c1.next(), c1.next()]
[ 3, 5, 7, undefined, 0, 2 ]
          

Prototype implementation

  • __proto__ internal property of object
    • points to another object
  • If property does not exist, search __proto__
    • Do this recursively until found
  • See here, here, and maybe here
  • Terminology
    • textbook: delegation-based inheritance
    • ECMAScript: prototype-based inheritance

Caution

  • __proto__ !=== prototype
  • All objects have __proto__
  • Functions have prototype
  • In constructor context, new object __proto__ is set to the function's prototype

Prototype Implementation


function Foo(y) { this.y = y; }
Foo.prototype.x = 10;
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};
var b = new Foo(20);
var c = new Foo(30);
          

Very Dynamic

  • Change __proto__ at runtime!

function UpCounter () { this.n = -1; }
UpCounter.prototype.next  = function () { return ++this.n; }

function DownCounter () { this.n = 1; }
DownCounter.prototype.next  = function () { return --this.n; }

var c = new UpCounter ();
c.next ();   // Returns 0
c.next ();   // Returns 1

c.__proto__ = DownCounter.prototype
c.next ();   // Returns 0
c.next ();   // Returns -1
          

Delegation Chain

Delegation Chain

  • The delegate object can also delegate

function ResetCounter () { this.reset(); }
ResetCounter.prototype.reset  = function () { this.n = -1; }
var c = new ResetCounter ();
c.next ();   // TypeError: c.next is not a function

function Counter () { this.n = -1; }
Counter.prototype.next  = function () { return ++this.n; }

ResetCounter.prototype.__proto__  = Counter.prototype
c.next ();   // Returns 0
c.next ();   // Returns 1
          

Implementation