Death to Javascript! Or, Writing Javascript with Ruby

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!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus