Javascript is so flexible that it doesn't have classes and doesn't need them. But lots of people really, really want to write classes. Javascript humors them, or tries to. It's pretty cool. It's easy enough for Java to fake a class with predefined elements. It's a little more work to hack in these things having actual types. And then it does backflips to add working member functions. All for idiots who can't learn a different programming style. It's pretty cool.
As we know, Javascript's main datatype is a generic object, with completely arbitrary member variables (which it calls properties). We often create object and properties all at once, Map-style:
var p1 = {"x":6, "y":9}; p1.x // 6 p1.y // 9
But this is only a shortcut. We can add more properties on-the-fly, merely by assigning to them. This adds an animal
property to p1
:
p1.animal="frog"; // we just created a new animal property for p1
Because of this, it's common to "construct" an object by starting with an empty and adding the properties we want, 1 at a time:
// this is the same as var p2={"x":7, "y":4} var p2 = {}; // empty object p2.x=7; // creates new property x p2.y=4; // ditto
The rules for properties are so loose that attempting to use a non-existant one isn't even considered an error.
var x=p1.notMadeYet;
assigns undefined
to x
.
Properties are so whacky that we can even change the types, or remove them:
p2.x=8 p2.x="seven"; // changed p2.x from int to string delete p2["x"]; // now p2.x is back to "undefined"
The craziest feature, to an OOP programmer, is that an
object can so lose track of its fields that we need a way
to find them. Object.keys(p2)
gives the names of p2
's fields as a list: ["x","y"]
.
Moving on to member functions, as we know, javascript variables can easily point to existing functions or anonymous ones, i.e: var f1=function(x) { return x*2; }
. Javascript properties can do the same, somewhat faking member functions:
// p as 2 "member functions": var p={}; p.chooseOne = Math.min; p.changeNum = function(var n) { return n+1; } p.chooseOne(5,8) // 5 p.changeNum(9) // 10
These are normal functions. p
happens to find them through its properties, but they're not member functions. Even so, they look like them and later, with some work and new special rules, we can make them act that way.
Javascript's first step towards pretending to have classes is
a convention of "return an object" functions. This Point
function is a quick way to make and
fill an "x,y,z-having object":
function Point(x,y,z) { var p = {}; p.x=x; p.y=y; p.z=z; return p; } var p1 = Point(3,8,1);
Point
is a perfectly ordinary function that returns objects. But it feels like a
class constructor, which is what we were going for.
new
hackTo class it up (pun intended), javascript added a new
keyword. Clearly, it has nothing to do with allocating dynamic memory. Instead, adding new
before a function call invisibly creates an empty object, locally assigned to this
, and
returns it for free. First a non-useful example using an ordinary function:
function honk(n) { console.log("HONK"+n); } // normal call: var h1 = honk(3); // HONK3 // h1 is "undefined" since honk has no return value // calling it with "new": var h2 = new honk(6); // HONK6 // h2 is {}, an empty string, created and returned by "new"
Calling it with it without new
, honk()
runs as normal. Adding new
created and auto-returned an empty object (which was accessable inside using this
).
The intended use of new
is to slightly simplify our old fake constructor functions. Here's a Dog
-making function meant to be called with new
:
// written to work with "new": function Dog(nm, old) { // we get this={} for free. Fill it in: this.name=nm; if(old>=0) this.age=old; else this.age=0; // "return this" is automatic } var d1 = new Dog("Rex",4); // runs the Dog function d1.name // Rex d1.age // 4
Now that really feels like a proper constructor! It turns out it also sort-of makes a Dog object since new
does one more thing. It adds the property "constructor" to the object with the name of the function (Dog) which was called (actually "constructor:Dog" is added several levels deep -- Javascript objects have a whole secret tree of built-in properties). More-or-less, we know d1
is a Dog since it gets a property saying it's one.
Backing up, regular old var d2={"name":"Rex", "age":4, "cName":"Dog"}
would have done almost the same thing. Also, new
still creates regular objects: they can still add properties -- d1.cost=34
or remove them -- delete p1["name"]
.
A fun fact about new
-wanting functions is that
they can be run without it. They will uselessly create globals:
var d3 = Dog("Sparky",7); // no "new", so no auto-return. d3 is undefined // Ooops! globals name and age were created and assigned: print(name); // "Sparky" print(age); // 70
We'll see why this happens, later.
Our first attempt at adding true member functions is expeanding the special this
rule. Whenever a function is called like p.f1(5)
javascript automatically sets local variable this
to the caller. In practice that means we can write almost real member functions if we always use this.name
and so on. Here we add an isPuppy
function to our Dogs:
// Dog creation function: function Dog(nm, old) { this.name=nm; this.age=old; this.isPuppy = function() { return this.age<=2; } } var d1=new Dog("Yeller", 4); if( d1.isPuppy() ) {...}; // this will work (it's false)
isPuppy
is still a normal, non-member function. But inside of it this
was set to d1
and this.age
got the correct age. It's basically a real member function.
To double-check, let's try it without using this
:
function Dog(nm, old) { ... // just "age" instead of "this.age": this.isPuppy = function() { return age<=2; } } var d1=Dog("X",9999); // d1 is very old, not a puppy var age=1; // isPuppy uses this age instead of the dog's age d1.isPuppy() // true!! 1<=2
What happened was that, as usual, isPuppy
run as a normal function. When it got to age<=2
it looked at the global age variable.
One more bit of weirdness is that we can have general-use member functions, shared by several classes. Here normal function ageString
assumes it will have a this
variable. Weasel and Owl are able to use it as a member function:
function ageString() { if(this.age<=3) return "young"; else if(this.age<=12) return "adult"; return "old"; } function Weasel(nm, old) { this.name=nm; this.age=old; this.wordForAge=ageString; // link to global function } function Owl(theBreed, old) { this.breed=theBreed; this.age=old; this.ageDescript=ageString; // link to global function }
For fun each class uses a different name, but that doesn't matter. w1.wordForAge()
works for weasels and o1.ageDescript()
works for owls. The ageString
function doesn't care what type this
is set to, as long as it has an age
.
Something funny we saw earlier, calling simply ageString()
is legal. It attempts to read from the
global age
. The special rule for this
is that it's always set to something. Either the caller, or the global space if there was no caller. This is why calling our fake constructor -- d1=Dog("Rex",8)
-- sets global name and age; this
was set to global. I can't think of a good use for the "set to global" rule.
So far we've been attaching member functions to objects -- each copy of Dog
has it's own isPuppy
member function. That's not a good way to do it and not how a real class does it. For real we want to store member functions with the class. Javascript has a system to do it that way. It has an unfixable problem, which we'll see later, but it's still cool to see the trick.
First some background: as we know all javascript objects can also have properties, including named functions. Our Dog
"constructor" can be assigned properties such as Dog.n=5;
. That's a seriously weird place to put a 5, but you can do it.
We'll use that trick to attach class member functions to the constructor. Roughly, we'll set Dog.isPuppy=function...
. The actual rule is to first create a property named prototype
and put all of your member funtions there.
A run-through of calling these new types of member functions: first you call d1.isPuppy()
. There's no property with that name, so the system checks d1
for the magic constructor
property. In d1
's case it has one (added when we called new Dog()
). The rules say we then jump to the function Dog
and check for Dog.prototype.isPuppy
. We then call that with this
, as usual, set to d1
. That's roughly how real members functions work, so pretty neat.
There are no special rules to create a new-style member function. Simply assign them:
// all Dogs get a "bark" mem-func: Dog.prototype.bark = function() { console.log("arf"); } // and one more using a member variable: Dog.prototype.isGoodName = function() { return this.name.length<=3; }; // the best dog names are 3 or fewer letters d1.bark(); // arf d1.isGoodName() // false (Spot is 1 letter too long)
Notice how, once again this
is required.
These remain normal functions using the special this
local to find the caller.
To be complete, prototype
can also hold static
variables. It's nothing special -- we've seen that any function can have properties -- but in our minds Dog
represents a class, so size
here feels like a class variable:
Dog.prototype.size=37; d1.size // 37 Dog.size // undefined. Too bad Dog.prototype.size // 37
It's seriously weird how a class static can only be referenced through an object, and not the class name like it should be. Oh, well.
this
is a hackWe should be excited that we've got something that feels and
works like a class. Except there's a serious bug: these member functions don't work if we call them through references; this
won't be set correctly. An example:
var isYoung1 = d1.isPuppy; // pointer to our "member" function if( isYoung1() ) {} // calling it through the pointer // ERROR -- no such global variable "age"
It fails since this
is set at the moment of the call. When we assign var isYoung=d1.isPuppy;
we completely loose all knowledge of d1
. The call isYoung()
is a normal function call and reverts to setting this
to "globals".
That problem may seem contrived, but other languages do this all the time. In javascript all sorts of frameworks might want you to pass in pointers to functions, often member functions. So we have a serious problem. And again, it's because this
is a hack. Javascript doesn't support member functions which truely operate in the environment of the object which called them.
This won't help us but it's fun to play with. Javascript alows you to assign this
manually, using the pre-made call
function. The input is whatever object you
want for this
(followed by the regular inputs). In this silly example we use d1
to find a function, but then call it on d2
:
d1.isPuppy.call(d2); // calls d1.isPuppy, but with d2 as "this" // tells us whether d2 is a puppy var isYoung=d1.isPuppy; isYoung.call(d1) // this works! Manually set "this" to d1
For fun (and as an example of sending additional inputs through call
) we can mimic the new
command using call
:
// Abusing a "constructor" function: var p={}; // an empty Point.call(p, 3,8,10); // assigns 3,8,10 into this=p
Recall that Point has lines like this.x=x
. We hand-set this
to p
, so it works! Notice how we didn't use new
here. It would be an error since Point.call
isn't a function, much less a constructor.
The point of this section is that this
is merely
another input to the function, passed in a non-standard way. It has none of the specialness it does in a true member function.
We'll need a quick review for this one. Like most fancy modern languages, Javascript can "lock in" context variables into a created function. Here makeAdder is a standard curried "create a 1-input function to add locked-in x" function:
// a function which creates and returns another function: function makeAdder(addMe) { return function(n) { return n+addMe; } } var a5=partialAdd(5); // a5 is a 1-input function that adds 5 var n1=a5(3); // 8 var a34 = partialAdd(34); // a34 is also a function, adding 34 a5(50) // 55 both have their own copy of addMe a34(50) // 84
Two things need to happen for this to work: we need to return a freshly created
function (which makeAdder
does), and it needs to use
a temporary "outside" variable (addMe
in this case). When those happen,
the new function "captures" the current value of addMe
. It's locked in as 5 in a5
and as 34 in a34
. This isn't something javascript added for classes -- it's a standard advanced language feature.
Our member functions will use this to lock-in the value
of this
. Our constructors will declare a local equal to this
and then use it in every member function:
function Dog(nm, old) { var self=this; // captures "this" this.name=nm; this.age=old; // using "self" instead of "this": this.isPuppy = function() { return self.age<=2; } }
self
isn't defined anywhere in isPuppy
-- it's an outside variable, which means it's locked to the currect Dog
in this copy. When we create another Dog
we'll get a new copy is isPuppy
locked to the new Dog.
To be sure we've got it, Let's make one that does more
and has an input. Instead of self
we'll use myDog
:
function Dog { var myDog=this; ... this.ageMe = function(years) { myDog.age+=years; } } d1.ageMe(2); // d1 age increased by 2 var dogOp = d1.ageMe; dogOp(-1); // d1 gets 1 year younger
This trick won't work with the prototype method. It relies on each object having a personal copy of the function, each with its own personal locked-in value for self
.
bind
This method also locks in this
, but using a new
command: bind
. It's essentially a shorter way of doing the previous trick. An example:
function Cat(nm, old) { this.name=nm; this.age=old; var tempOlderThan = function(c2) { return this.age>c2.age; } // this is it: this.olderThan = tempOlderThan.bind(this); }
The last line gives us a new function, running tempOlderThan
with this
locked to the currect Cat
.
We can also do it without an intermediary:
this.olderThan = (function(c2) { return this.age>c2.age; }).bind(this);
The convention is to double-assign. No reason except people think it looks cool:
// we will override this one: this.olderThan f1=function(c2) { return this.age>c2.age; } this.olderThan = this.olderThan.bind(this); // begins and ends with "this"!
bind
is sort of a silly command. Javascript users already fixed
things using the self
trick, then javascript central decided to add bind
, which does the same thing in fewer keystrokes. Even so, bind
is now the official way of creating working member functions.
For fun, bind
can also lock-in parameters, from the left. We can us it to make our "add-N" functions:
function add(a,b) { return a+b; } // lock-in and remove input "a" with 5 then 34: var add5=add.bind(null,5); // null is a dummy value for "this" var add34=add.bind(null,34) var n=add5(8); // 12 add34(4) // 38
The result is a function with the first few parameters removed. add5
and add34
take only b
as their input, with a
locked-in by the bind
.
class
shortcutJavascript now allows you to use a class syntax to create the stuff we've seen before. "constructor" creates the same old construction function from before. Notice how we still need to bind this
:
class Goat { constructor(nm, old) { this.name=nm; this.age=old; this.sayName = this.sayName.bind(this); } sayName() { console.log(this.name); } } var g1 = new Goat("Billy",7);
I don't even know what that funny syntax is for the base sayName
-- something they made up just for this. It appears to only be usable to assign to in the constructor.
Writing it this way has 1 bonus: leaving out new
(as in var g1=Goat();
) is now an error (in the function-style method it's still junk, but not an error).
I suppose this is for people who won't be happy until they can declare a class using familiar syntax. As of this writing, javascript is adding more hacks to make it look more class-like.
To sum up, we've got 2 similar ways of making a pretend class: a "constructor" function, or class-style. You'll see a mix. In fact, the React.js framework has you use both, using "class" as a special signal.
A funny thing is how the first this
-setting trick (where d1.myFunc()
sets this
) is outdated. Our new tricks manually fix it in place. Attempts to make javascript feel like it has classes were so frenzied they step on each other.
I have to go with my first impression here. Python gets by just fine with purely informal interfaces and malleable objects. But a bunch of idiots who learned Classulla-2 in Jr. High got the ear of the javascript board and made them crap it up with this stuff.
Comments. or email adminATtaxesforcatses.com