Skip to content

Instantly share code, notes, and snippets.

@pjlsergeant
Last active May 30, 2021 14:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pjlsergeant/5031faeabe322f49f1b242e159aa5309 to your computer and use it in GitHub Desktop.
Save pjlsergeant/5031faeabe322f49f1b242e159aa5309 to your computer and use it in GitHub Desktop.
Quick JS OO Refresher

Quick JS OO Refresher

I wrote this trying to get closures and function properties straight in my head, and it was useful for that. I'm not sure if it has any value for showing how JS's OO system works, but it might.

Can you guess what this prints?

[function(){ console.log( this )}][0]();

Functions are Objects

All JavaScript data is typed, and you can find out the type using typeof:

> typeof 5
'number'
> typeof true
'boolean'

All values that aren't simple scalars ("primitives") are objects, and have a typeof 'object', with one special edge-case: functions are objects, but respond to typeof as function:

> typeof []
'object'
> typeof new Date()
'object'
> typeof( () => {} )
'function'

This is merely a special shorthand for Functions ... every Function constructor is derived from Object constructor.

But because a function in JavaScript is also an object a function can have properties:

// An object with properties
let obj = {};
obj['foo'] = 'bar';
console.log( obj.foo );

// A function with properties
let fun = () => {};
fun['foo'] = 'bar';
console.log( fun.foo );

Note that this is distinct concept from a closure:

function counter() {
  let x = 0;
  // this function closes over x
  return function() { return ++x }
}

let foo = counter();
console.log(foo()); // 1
console.log(foo()); // 2 -- function has closed over x and remembers its val

let bar = counter();
console.log(bar()); // 1 -- each new call to counter() creates a new closure...
console.log(foo()); // 3 -- ... without affecting the old one

// but note that x is _NOT_ a property of either counter() or the function
// returned by invoking it:
console.log( counter.x ); // undefined
console.log( foo.x );     // undefined

// and we can even set a different x as a property of both that has no effect
// on the closed-over variable x:
counter.x = 'new';
foo.x     = 'now';

console.log( counter.x ); // "new"
console.log( foo.x );     // "now"

console.log( foo() ) // 4

this means "caller"

JavaScript's this keyword -- called inside a non-class-constructor function -- returns the object from which the function was called:

// there's some special-casing around Object [global] that's dull and fiddly
// but that strict-mode nicely side-steps
"use strict";

function cat() { console.log( "cat: ", this ) }
function dog() { console.log( "dog: ", this ) }

cat.dog = dog;

cat(); // cat: undefined -- wasn't called "from" anything
dog(); // dog: undefined -- wasn't called "from" anything

// was called from `cat`, so `this` is `cat`
cat.dog(); // dog: [Function: cat] { dog: [Function: dog] }

// was called from inside a newly created array
[dog][0](); // dog: [ [Function: dog] ]

// `apply` allows us to set `this` to something else
dog.apply([1,2,3]); // dog: [ 1, 2, 3 ]

We can use this to start to bolt together things that look a bit like traditional OO objects:

const goes = function() { console.log( this.sound ) }
// Normal object with `sound` and `goes` properties
const dog = { sound: "woof", goes: goes }
const cat = { sound: "miao", goes: goes }

dog.goes(); // woof -- goes() is called with `this` set to `dog`
cat.goes(); // miao

Prototype-based inheritance

Manually assigning a goes property for each object we want to create is a hassle, but JavaScript allows us to specify that an object inherits properties from a parent object that's a prototype for it:

const goes = function() { console.log( this.sound ) }
const animal = { goes, alive: true }; // bare `goes` = shorthand for goes:goes

// dog doesn't have its own goes property
const dog = { sound: "woof" }
// but we tell property-resolution where else it can look
dog.__proto__ = animal;

const cat = { sound: "miao" }
cat.__proto__ = animal;

dog.goes(); // woof
cat.goes(); // miao

// note there's no distinction between a property or a function that's a
// property, so we can look up simple properties in the same way we can look
// up the "methods"
console.log( dog.alive ); // true

... but again, we've got some pretty hassle-ish code as we're manually assigning the scary-looking and very definitely private-looking __proto__ for each of our new objects. Let's start by manually writing a little factory for these objects:

const goes = function() { console.log( this.sound ) }
const animal = { goes, alive: true };

function factory( sound ) {
  const self = {};         // Creates a blank, plain JavaScript object
  self.sound = sound;      // add the `sound` attribute
  self.__proto__ = animal; // Link this object to the animal object
  return self;
}

const dog = factory( 'woof' );
const cat = factory( 'miao' );

dog.goes(); // woof
cat.goes(); // miao

factory() here looks like it could be made pretty generic, but it's still got some animal-specific logic in it -- it would be better if we could tie instantiation effects like self.sound = sound to animal itself, and then have a completely generic factory. Let's add a _setup function:

const goes = function() { console.log( this.sound ) }
const setupAnimal = function(sound) { this.sound = sound };
const animal = { _setup: setupAnimal, goes, alive: true };

function factory( obj, arg1 ) {
  const self = {};          // Creates a blank, plain JavaScript object
  self.__proto__ = obj;     // Link to the parent function
  // Run _setup(), but set `this` to be `self` first
  obj._setup.apply(self, [arg1]);
  return self;
}

const dog = factory( animal, 'woof' );
const cat = factory( animal, 'miao' );

dog.goes(); // woof
cat.goes(); // miao

The "magic" here is the use of apply(), which allows us to alias this inside setupAnimal to be the newly created object.

Now for something new

We have built at this point all the pieces we really need of an OO system, and if we were to set factory() in the global object where everything can access it, we're pretty much job done now. However, if we really want to show off how 'leet we are, well, this code is far too readable, and it's far too obvious that this is "bolted on" OO.

Given that a function itself is an object, we could have our setupAnimal() itself be the object that we're using as the base, and do away with having defined it separately:

const animal = function( sound ) {
  this.sound = sound;
  this.goes  = function() { console.log( this.sound ) };
  this.alive = true;
}

function factory( obj, arg1 ) {
  const self = {};      // Creates a blank, plain JavaScript object
  self.__proto__ = obj; // Link to the parent function
  // Run the object itself as the setup, on the newly created self
  obj.apply( self, [arg1] );
  return self;
}

const dog = factory( animal, 'woof' );
const cat = factory( animal, 'miao' );

dog.goes(); // woof
cat.goes(); // miao

You've probably guessed by now exactly where this is headed, and realized that our factory() function is a reimplementation of the new keyword. In fact we can just call new already on this animal() function directly to get the same effect:

const animal = function( sound ) {
  this.sound = sound;
  this.goes  = function() { console.log( this.sound ) };
  this.alive = true;
}

const dog = new animal( 'woof' );
const cat = new animal( 'miao' );

dog.goes(); // woof
cat.goes(); // miao

Side-note: our factory() function doesn't correctly set the object's constructor property, as I didn't want to muddy the waters. You could easily add that to the definition of factory() as self.constructor = obj. See: Object.prototype.constructor

NB: JavaScript programmers prefer to name their functions that they'll use as objects with a capital letter at the front, for example Animal not animal.

Arrow functions

Arrow functions are awesome because they allow us to pretend to be real programmers writing lambdas and because they save a jaw-dropping 5 characters of source code. However, they also close over this based on the scope the Arrow function is defined within.

const normal = function() { console.log( this ) };
const arrow  = () => console.log( this );

// `this` becomes the object from which the function was called for a normal
[normal][0](); // [ [Function: foo] ]

// `this` is whatever it was when `arrow` was defined, which means it's the
// same `this` we have in scope right now...
[arrow][0](); // {}
this.foo = 'bar';
[arrow][0](); // { foo: 'bar' }

/* In the browser, `this` outside a function will be the global object, in
Node.js it'll be set to the same value as whatever `exports` is */

Classes

JS now has real syntax to deal with classes

Further Reading

John Resig's precursor slide-show to his Ninja book is a fun read and covers many of the same themes as this does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment