Okay, first off, don't let the title of this post mislead you. We love Javascript. Say it with me: "We love Javascript".
Now that we've gotten that out of the way, we can admit it: learning Javascript after learning Ruby is hard. Those of us that speak Ruby as a first language are spoiled by the eloquence of that language. It is a language that really speaks for itself––the rules, syntax and design patterns of Object Oriented Ruby can feel like common sense (once you get the hang of them). Jumping from Ruby into Javascript, the land of non-sensical semicolons, can be a shock, to say the least.
As an instructor at, and graduate of, the Flatiron School, where we learn Ruby, Rails and then Javascript, I've often found looking at Javascript through a Ruby lens to be a helpful tool for both my students and myself.
One point of confusion, though, when Rubyists approach Javascript for the first time, is the difference between a Javascript object, a Ruby object and a Ruby hash. After all, a Javascript object can look just like a Ruby hash. If a Javascript object looks just like a Ruby hash, and can even be defined or declared in a similar manner to the way in which we define a Ruby hash, how can a Javascript object have a concept of its own properties and behaviors?
In this post, we'll take a closer look at Javascript objects and we'll re-create a Javascript object using a Ruby hash and a little bit of magic in an effort to elucidate the nature of Object Orientation in Javascript.
Javascript Objects: A Brief Overview
We can declare an object in Javascript like this:
var book = {title: "A Clash of Kings", author: "George R.R. Martin"}
This is called the object literal. We've use it to declare an object with two properties, title
and author
. We've stored our object in a variable, book
, and we can now access the values stored in each of those properties using dot notation:
book.title
=> 'A Clash of Kings'
book.author
=> 'George R.R. Martin'
At this point, you might be thinking that a Javascript object differs little from a Ruby hash. So far they look the same and even behave similarly––they are both containers for named data.
Unlike a Ruby hash, though, we can also add behavior to our Javascript object. We can give our object a method by setting a named property equal to a function:
var book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
bookInfo: function() {
console.log(this.title + ", by: " + this.author)
}
}
Here, we've set bookInfo
equal to a function that will log to the console some information about the book. Now, let's see what happens when we invoke that bookInfo
function:
book.bookInfo();
Will log to the console:
A Clash of Kings, by: George R.R. Martin
Notice the use of this.title
and this.author
inside of our bookInfo
function. Here is the key difference between a Ruby hash and a Javascript object.
Javascript objects are smart, they have a concept of "self" (if you want to think about it from a Ruby point of view, which you do) and can look up the properties associated with themselves. How can they do that? Let's take a closer look.
The this
Keyword Inside a Function
Used inside a function, this
will be bound to the object that the function is invoked on, just like how the self
keyword refers to the object that method is called on inside a Ruby method.
Let's see what happens when we define the same object, but without our this
keyword in the function definition:
var book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
bookInfo: function() {
console.log(title + " by: " + author)
}
}
What will happen when we try to invoke the bookInfo
function?
book.bookInfo();
We'll see:
ReferenceError: title is not defined
When we were using the this
keyword, this
was bound to the object upon which we are invoking the function:
{ title: 'A Clash of Kings',
author: 'George R.R. Martin',
bookInfo: [Function] }
This object has named values bound to it and has the ability to look up those values using dot notation. That is what is happening when we call this.title
and this.author
. This is a huge contrast to a Ruby hash which doesn't have a concept of self
and which can't have behavior, or methods, associated to it.
Now that we have a solid understanding of what a Javascript object is, how to define it and give it properties and behaviors, let's build it in Ruby!
Building Javascript Objects with Ruby
At this point you might be thinking: if a Javascript object can have properties and behaviors associated with it, and if it has a concept of "self", then it is just like a Ruby object, as produced by any of the classes we might define in an Object Oriented Ruby program. If you're thinking that, you're right.
You might therefore be led to wonder, if a Javascript object is just like a Ruby object, why confuse yourself with comparing it to a Ruby hash.
Well, in Ruby, to produce an object with properties and behaviors, we have to define a class with instance methods:
class Book
attr_accessor :title, :author
def book_info
puts "#{self.title}, by: #{self.author}"
end
end
This is markedly different from Javascript's ability to use the object literal syntax to declare a stand-alone object. Javascript's object literal syntax is much more similar to how we declare hashes in Ruby. So, by taking a deeper dive into Ruby hashes, and using some other Ruby tools to change the behavior and abilities of those hashes, we can gain a better understanding of the power and flexibility of Javascript objects.
Now that we all agree that this will be a super helpful and enlightening exercise, let's try to recreate the behavior of a Javascript object using a Ruby hash.
Ruby Hashes are Not Javascript Objects
What would happen if we tried to treat a Ruby hash just like our Javascript object?
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: puts "#{title}, by: #{author}"
}
If we try to execute the file containing the above code we'll immediately see a syntax error. Setting a hash key to a value of a method call just isn't valid Ruby.
So, how do we store a chunk of code to be execute later? We can use a Proc for this purpose. Let's refactor:
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: proc {puts "#{title}, by: #{author}"}
}
What happens when we try to call our Proc?
book[:book_info].call
=> undefined local variable or method `title' for main:Object (NameError)
We get a NameError. Our Proc, even when it is called on our book
hash, has no idea what title
and author
are!
Recall that we received a similar error when we tried to define our bookInfo
function on our book
Javascript object without using the this
keyword. Let's give in to a little wishful thinking and trying employing self
in our Proc in a similar manner:
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: proc {puts "#{self.title}, by: #{self.author}"}
}
book[:book_info].call
=> undefined local variable or method `title' for main:Object (NameError)
Looks like we get the exact same error as before. That is because self
, in this context, refers to the main object. Unlike inside of a function that we associate to a Javascript object, it is not bound to the object to which the Proc is associated.
We need to find a way to execute or evaluate the code in our Proc in the context of the hash to which that Proc has been associated. In order to do that, we need access to the moment in time at which we are executing that Proc, i.e. the moment we call book[:book_info]
. Therefore, we need to hook into the []
hash method.
Executing the Proc
How can we hook into the []
method? One method that comes to mind is to monkey patch the Hash class and overwrite the []
method. However, this is a terrible idea. Repeat after me: "We are NOT going to monkey patch the Hash class. Ever." (There seem to be a lot of mantras in this post...)
Okay, now that we've established that we should not overwrite the most common hash method and therefore confuse every single person who uses our code, let's move on.
Instead, we'll extend the functionality of our hash with a module.
require 'pry'
module JSAble
def [](key)
binding.pry
end
end
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: proc {puts "#{self.title}, by: #{self.author}"}
}
book.extend(JSAble)
book[:book_info]
Note that we've extended the JSAble module into our Hash instance, book
, by using the #extend
method.
Now, will a binding.pry
in the []
method, we can drop right into the environment of our code at the moment that book[:book_info]
is called.
Let's think a little bit about what we are trying to do here: We need a way to execute the code in the Proc stored in the book_info
key of our book
hash, such that the code in that block knows what object the [:book_info]
method is being called on. In other words, we need a way to trick our book
hash into having a sense of self
, so that when the code in our Proc is executed, the hash itself can respond to the request for its title
and author
.
Let's run our code in the terminal and drop into our Pry console via the binding.pry
.
Right now our code looks like this:
# death_to_javascript.rb
require 'pry'
module JSAble
def [](key)
binding.pry
end
end
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: proc {puts "#{self.title}, by: #{self.author}"}
}
book.extend(JSAble)
book[:book_info]
And we'll execute it in the terminal:
ruby death_to_javascript.rb
That should drop us right into the Pry console. How to we access our Proc from inside the []
method? Well, we ended up inside the []
method because we called book[:book_info]
. So, the super
keyword will give us the original return value of [:book_info]
:
Now, how can we execute the code in the Proc such that it is aware of the object, in this case our book
hash, that we are operating within the context of? If only there was a way to call the Proc on the hash.
As a matter of fact, we can do exactly that with the #instance_eval
method.
Using #instance_eval
The #instance_eval
method
Evaluates...the given block, within the context of the receiver (obj). In order to set the context, the variable
self
is set to obj while the code is executing*
This is exactly what we are looking for! We can call #instance_eval
on our book
hash and give it an argument of the code in our Proc, as a block. The code in the block will be executed and the self
keyword in the code in that block will be set equal to the object on which we are calling #instance_eval
, our hash.
Let's do it:
# death_to_javascript.rb
require 'pry'
module JSAble
def [](key)
self.instance_eval(&super)
end
end
Side note: remember that &proc
is how we can execute the code stored in a Proc, as a block.
There's only one problem. The code in our Proc, to refresh your memory, looks like this:
puts "#{self.title}, by: #{self.author}"
So, executing our []
method as is will raise the following error:
NoMethodError: undefined method `title' for #<Hash:0x007fb4ba8c3b30>
Although our use of #instance_eval
absolutely worked as a way to bind self
to the Hash instance that we are calling []
on, *there is not such #title
method on our hash. Therefore, self.title
still blows up.
Hmmm...if only there was a way to create a hash-like structure that allows us to store attributes that are retrievable via dot notation...if only...
Introducing OpenStruct
An OpenStruct is a data structure, similar to a Hash, that allows the definition of arbitrary attributes with their accompanying values. This is accomplished by using Ruby’s metaprogramming to define methods on the class itself.*
In other words, an OpenStruct can contain the name data that our hash contains, but, because it inherently implements #method_missing
override, we can use dot notation to look up its attributes, without our program blowing up at us!
Let's use it:
require 'ostruct'
require 'pry'
module JSAble
def [](key)
os = OpenStruct.new(self)
os.instance_eval(&super)
end
end
Here, we create a new OpenStruct giving it the argument of our book
hash, referred to by self
. Then, we call #instance_eval
on our new OpenStruct. Inside the Proc that is executed when we call &super
, self
will be set equal to the receiver of #instance_eval
: the OpenStruct. OpenStructs allow for us to call any method we want on them. If the OpenStruct can find an attribute that matches the name of the method called, then the value of that attribute will be returned, otherwise, nil
will be returned.
For example:
os = OpenStruct.new({title: "A Clash of Kings", author: "George R.R. Martin"})
os.title
=> "A Clash of Kings"
os.name
=> nil
os.the_700th_president_of_the_united_states?
=> nil
Therefore, the code in the Proc that we are executing when we call &super
:
puts "#{self.title}, by: #{self.author}"
Understands self
to be equal to our OpenStruct, which will respond to #title
and #author
with the values stored in those attribute names.
Now, if we run this code:
require 'ostruct'
require 'pry'
module JSAble
def [](key)
os = OpenStruct.new(self)
os.instance_eval(&super)
end
end
book = {
title: "A Clash of Kings",
author: "George R.R. Martin",
book_info: proc {puts "#{self.title}, by: #{self.author}"}
}
book.extend(JSAble)
book[:book_info]
We'll see:
A Clash of Kings, by: George R.R. Martin
There's only one problem with the code we've written so far. If the value of super
, within a given call to []
, isn't a Proc, our code will break. Let's fix that with some simple logic:
def [](key)
value = super
if value.is_a?(Proc)
os = OpenStruct.new(self)
os.instance_eval(&super)
else
super
end
end
Great, now our []
method will behave as expected when the value of the key we are calling it with is not a Proc.
JS-ifying Our Hash
We're not quite done building a Javascript-like object out of our Ruby hash. In Javascript, we can use dot notation to access the properties and invoke the functions of a given object. Our hash still must be accessed via the []
method. Yuck.
Let's fix that by implementing our own #method_missing
in our module:
module JSAble
def [](key)
value = super
if value.is_a?(Proc)
os = OpenStruct.new(self)
os.instance_eval(&super)
else
super
end
end
def method_missing(m, *args, &bloc)
if self.keys.include?(m)
self[m]
end
end
end
In our #method_missing
override, we check to see if the method being called on the hash has the same name as any of the keys of that hash. If so, use the []
method to get the value of that hash key. This will allow us to access values of and execute the Proc stored in any hash keys:
book.book_info
=> A Clash of Kings, by: George R.R. Martin
=> nil
puts book.title
=> "A Clash of Kings"
It worked!
One last thing. You might be thinking: if we over-wrote the #method_missing
of our book
hash to allow for the returning key values using dot notation, do we really need our OpenStruct? In fact, we don't! Let's refactor:
module JSAble
def [](key)
value = super
if value.is_a?(Proc)
self.instance_eval(&value)
else
super
end
end
def method_missing(m, *args, &bloc)
if self.keys.include?(m)
self[m]
end
end
end
And that's it! The above code will behave exactly like our earlier implementation with OpenStruct, but we won't be generating an OpenStruct every time we want to execute the Proc stored in the :book_info
key. This is especially good given that the garbage collector doesn't do a great job of getting ride of OpenStructs, so their usage can be expensive.
Conclusion
I hope this exercise in JS-ifying Ruby has shined some light on how powerful and flexible Javascript objects really are. All of the work we had to do to get a simple Ruby hash to become self-aware, Javascript gives us for free!
Last but not least: thanks to Steven Nunez for the whirlwind introduction to the code that inspired this blog post!