Classes vs. Constructors
Metaobjects in Ruby and JavaScript
We've been spending the last few weeks taking the first steps in harnessing the power of object-oriented programming, and in Ruby the key to orienting those objects is the concept of "class". This is the mold in which a developer defines a set of characteristics, including values, states and functions that are all bundled into the notion of what it means to be an object of said class. Any new instance of your class will take on these characteristics, and in doing so will give the programmer power and efficiency that wouldn't be possible with loads of disconnected code.
It turns out JavaScript has similar built-in functionality in the form of the "prototype", which is built using a "constructor" function. This has key differences from a Ruby class, mostly philosophical from a beginner's perspective, but exists for essentially the same purpose. Class and prototype are both examples of a larger concept...
metaobjects!
...which are any objects designed to describe or define other objects. That seems pretty straightforward. Let's meet a couple of these now.
Ruby:
class Immortal
def initialize(name, power, era_of_origin)
@name = name
@power = power
@era = era_of_origin
@life_expectancy = Float::INFINITY
@head = true
end
def behead(inferior_immortal)
if inferior_immortal.head?
inferior_immortal.head = false
@power += inferior_immortal.power
end
end
end
With this class, a new instance can be created like this:
highlander = Immortal.new("Connor MacLeod", amassed_power, "16th Century Scotland")
Our new object highlander
is now defined by our class, so like all Immortals, its life_expectancy
is Float::INFINITY
and it has a method for behead
. Not bad - let's see this in JS.
JavaScript:
function immortal(name, power, eraOfOrigin) {
this.name = name;
this.power = power;
this.era = eraOfOrigin;
this.lifeExpectancy = Infinity;
this.head = true;
this.behead = function(inferiorImmortal) {
if(inferiorImmortal.head === true) {
inferiorImmortal.head = false;
this.power += inferiorImmortal.power;
}
}
}
Turns out these are pretty similar. Once again, let's make a new object that abides by this blueprint:
var highlander = new immortal("Connor MacLeod", amassedPower, "16th Century Scotland");
Essentially the same as Ruby. Most of the syntax differences are consistent with general language translation, but two are noteworthy. First, the instance variables have different notation. In Ruby anything defined like @variable
holds meaning throughout the class definition and is accessible by an instance of the class. In JS, they're defined with this.variable
. this
can be thought of as referring to "this instance" for each new object created of the class.
Second, whereas we had a method for initialize
in the Ruby class, our JavaScript constructor handled these steps right away. Any time an object is created, whether you've created a custom class for it or not, it goes through an initialization procedure, which attaches any values or methods to it at its inception. Notice that the JS constructor begins defining the prototype with function immortal()
- while Ruby names a class with a title and then has a neatly contained initialize
method. But the difference is purely syntactical, because both metaobjects run an initialization as the first step in instatiation.
How about something that actually matters then? JavaScript has a way to append a function to the prototype outside the constructor:
immortal.prototype.quickening = function(immortalsArray) {
if(immortalsArray.length === 1) {
this.knowAllHumanityThoughts = true;
}
}
Just got a sweet new ability! This will apply to all new instances of the prototype.
The biggest difference between these two metaobjects is one that's not apparent in normal surface interactions in the code. The way in which they are defined under the hood is fundamentally different for Ruby and Javascript. We'll start with Ruby.
Because "every thing in Ruby is an object" it can be hard to pick apart how objects interact with one another. Classes are objects themselves, and a class like our Immortal
is an instance of the class Class
. So if highlander
is an instance of class Immortal
, Class
is its "superclass". It keeps going too:
p Immortal.ancestors
=> [Immortal, Object, PP::ObjectMixin, Kernel, BasicObject]
This hierarchy follows the Ruby "object model" and gives order to the definition of each subclass. Kernel
inherits its properties from BasicObject
and so on down the list to Immortal
. We can see this in action by listing methods available to our instance:
p highlander.methods
=> [:behead, :pry, :__binding__, :pretty_print, :pretty_print_cycle,
:pretty_print_instance_variables, :pretty_print_inspect, :nil?, :===, :=~, :!~,
:eql?, :hash, :<=>, :class, :singleton_class, :clone, :dup, :itself, :taint,
:tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze, :frozen?, :to_s,
:inspect, :methods, :singleton_methods, :protected_methods, :private_methods,
:public_methods, :instance_variables, :instance_variable_get,
:instance_variable_set, :instance_variable_defined?, :remove_instance_variable,
:instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?,
:extend, :at_exit, :display, :require, :method, :public_method, :singleton_method,
:define_singleton_method, :object_id, :to_enum, :enum_for, :pretty_inspect, :==,
:equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]
waaaaaaay too much information, and I apologize. But the point is this is how Ruby is dealing with each object through that object's class structure. Javascript objects are conceptually different from their inception. A JS object isn't instantiated with an internal state bundled up with attributes from a legacy of superclass ancestors. When highlander
was initialized, it had one function: behead
. And then we gave it another using prototype notation: quickening
.
It probably doesn't seem like a big deal to Connor MacLeod of clan MacLeod whether or not he has access to :pretty_print
(though :taint
and :untrust
seem ominously useful). But even if the ramifications seem vague to me at this point in my understanding of the two languages, an idea of how different the fundamental structures of the way they approach object-oriented programming will be important to keep in mind as we dive deeper in the coming months.