Wow -- javascript is really not Object Oriented

home

Adding classes to javascript is my new favorite language hack. Let's start with a small review of basic javascript. Its main datatype is a generic object which can have any sort of attributes added or removed:

var p = {}; // empty object
p.x=7; // adds an x field
p.x=9;
p.y="frog"; // add a y field
delete p["x"]; // x field is gone

var w = p.x + " " + p.y; // undefined frog
p.y=8; // type changed from string to int. No problem

Very flexible. Variables have no fixed type; using one that doesn't exist isn't even an error. The craziest feature, to an OOP programmer, is how you often need to find your current fields, ["x","y"] in this case, using Object.keys(p).

You can pretend to make a class by having a reference function make a sample. It counts as documentation. This lets us know that a Point should have x,y,z number fields:

function Point() {
  var p = {};
  p.x=0.0; p.y=0.0; p.z=0.0;
  return p;
}

var p1 = Point(3,8,1);

There's still no actual Point class. The result of Point() is only an object. Anyone can add more fields, or change x to a string. But this is useful since anyone can find the Point function and figure out what Point's should be like.

Here's the new constructor-looking constructor. It does the same thing, but looks more object-oriented:

// this is a new constructor:
function Dog() {
  this.name="Spot";
  this.age=2;
}

// using "new" before a function call creates an empty object and sets "this"
var d1 = new Dog("Spot",4);

The thing I love is that it merely looks different. Javascript added new and this. But we're still taking an empty object and bolting on fields. The end result is still an object. We can still do crazy stuff like d1.age="old"; or d1.oil="WD20";. We can still make a Dog with simply var d2={name:"Rover", age:6}.

The new classes in javascript are the same arbitrary pile of fields as they always were.

Or not, since they also have member functions. This adds isPuppy to Dog's:

function Dog() {
  this.name="Spot"; this.age=2;
  
  this.isPuppy = function() { return this.age<=2; } // a member function, sort of
}

if( d1.isPuppy() ) w="grow up, d1"; // this will work

That seems pretty convincing, but it's still the same old thing. isPuppy is another field, which happens to currently be aimed at a function. We're merely calling a function. But wait, isPuppy directly uses the fields of its object, through this.age. That's new, and seems pretty OOP. But it's not.

Let's back up. In a normal OOP language, member functions automatically operate inside of the object calling them. They can use this, but don't need to. Javascript is actually using this to fake it. To see this, here's some wrong javascript. it tries to set its own x and y, but accidentally sets globals:

function Point() {
  this.x=0; this.y=0;
  this.set = function(newx, newy) { x=newx; y=newy; } // this is wrong
}

p1 = new Point();
p1.set(5,9); // ?? p1 is 0,0
// global x and y are 5 and 9

It makes perfect sense if you know that javascript doesn't have member functions. p1.set(5,9) has no knowledge of p1, so clearly can't affect it.

It turns out this in javascript is a hack. It's a bonus variable, set to the current environment during each call. d1.isPuppy() sets it to d1. Inside of isPuppy, this.age<=2 is following a pointer to d1. But in doStuff(6) it's also set, to global.

The this-hack is a neat trick, but won't always work. Suppose we call d1.isPuppy() through a pointer. The system gets confused -- there's no d1 in front of the actual call, so this is set to global space:

var isYoung = d1.isPuppy; // alternate way to find d1.isPuppy
if( isYoung() ) {} // error: can't find global "age"

So this isn't reliable. They added a trick to make member functions work, which fails half the time. But that's fine, we have another hack. But first some warm-up. Javascript can save contexts. Here's a curried (2-part) add:

function partialAdd(x) { return function(y) { return x+y; } }

var a5=partialAdd(5); // a5 adds 5 to its input
var n1=a5(3); // 8

After we make it, a5 is: function(y) { return x+y; } with x locked to 5. Functions have the ability to carry around extra vars from where they were made. The original 5 in the first call is long gone, but a5 holds a copy. Pretty cool.

We can abuse context-saving to make a working member function. The is the official way to make classes:

function Dog(name, age) {
  var self=this; // captures "this" in a closure
  this.name=name; this.age=age;
  this.isPuppy = function() { return self.age<=2; } // using captured "self" varialble
}

This is so cool. isPuppy is a normal function, with no special knowledge of any Dog, except it has one saved context variable, aimed at the Dog from when we made it. As long as we use self-dot everywhere, it will work. As a bonus, some users probably assume self is a keyword.

For fun, let's make another pretend member function. This creates a function with a saved dog, which can be run to age it:

function dogAger(d) {
  if(d.age === "undefined") return null; // error checking
  
  return function() { d.age++; }
}

var ageIt = dogAger(d1); // ageIt is locked into aging d1
ageIt(); ageIt(); // d1 is now 2-years later

ageIt acts like a member function. d1.ageMe=dogAger(d1) make the same thing, but inside of d1. The location doesn't affect how it works.

Finally, and this is awsum, the new javascript gives you an option for adding a member function to the class in general, except it doesn't work:

Dog.prototype.ageMe = function() { this.age++; }

Since it uses this, it only works with a direct call like d1.ageMe(), but not if we call it through a pointer. We can't add the self trick. For that we needed to hand-make functions for each dog, giving each their own personal self.

Just one more. We're even allowed to explicitly set this through an explicit call command, since Why Not? Here d1.isPuppy tells you whether d2 is a puppy:

if( d1.isPuppy.call(d2) ) {} // "call"'s 1st input is the value for "this"

It makes sense if you think of d1 as being the way to find isPuppy. Then, since we know isPuppy was written to use this, we choose what to run it on, inside of call.

To sum up, the new new command is merely a shortcut. It works like:

var p = new Point();

// or:
var p={}; Point.call(p);

The new this is simply an environment variable.

The end result is you're still using javascript, but if you really, really want to, you can pretend it's an OOP language. But javascript is really, really not OOP.

 

 

 

Comments. or email adminATtaxesforcatses.com