loading...
Cover image for Objection! Let's play with primitives in JavaScript

Objection! Let's play with primitives in JavaScript

martingaston profile image Martin Gaston ・4 min read

I like to spend lazy weekends eating my body weight in Pringles, playing videogames and also really digging into some of the fundamentals of programming languages to get an appreciation for what's going on. I've been fascinated by methods and primitives in JavaScript lately, spurred on as I look to branch out a deeper understanding of standard data types in both object-oriented and functional patterns.

While you certainly don't need to worry about a lot of this - it's really something the language largely abstracts away from you behind its individual implementation - it's nice to get a sense of what's going on.

I'm going to be using JavaScript for most of this, but also with little cameos from Ruby and Elixir, though you certainly don't need to know either of those languages.

Let's play with some JavaScript:

const foo = 'foo'
typeof foo
// 'string'

Consider the humble string, a proper salt of the earth, workhorse of data type. Strings are model citizens. They work hard all day and don't ask questions, and as long as they've got enough cash left over to take the family on a couple of holidays a year they're pretty contented.

Over on the fields of JavaScript, a string is one of its six primitives, which are kind of like the language's Infinity Stones - string, number, boolean, undefined, null and symbol. Primitives in JavaScript are immutable, although can be replaced by other primitives with assignment. Primitives are lightweight, like the delicious Total 0% of data types, and don't have methods or properties - or the extra processing needs that come with them.

Wait a second, woah woah woah thank you very much, how come you can do this then?

foo.toUpperCase() // "FOO"
foo.length // 3

Well, with the exception of undefined and null, each primitive also has a complementary primitive wrapper object that, perhaps unsurprisingly, wraps around it - so that'sString, Number, Boolean and Symbol.

When you're as smart as Kyle Simpson (❤️) you can actually dig deep into what's going on under the hood, but essentially the String object (which still has its own constructor) will create an object - so not a primitive - around a given string primitive, which is still accessible with the valueOf method. This, I believe, is also loosely analogous to the implementation of primitives in Java, but I'm certainly no expert there: the most I know about Java is the FizzBuzz Enterprise Edition.

const bar = new String("bar")
typeof bar // 'object'
typeof bar.valueOf() // 'string'
bar.valueOf() === "bar" // true
bar === "bar" // false

Now, JavaScript will also handle this wrapping (also commonly called boxing or autoboxing) automatically when you attempt to access a method or property on a primitive, and then immediately following its evaluation it will automatically fling this new, temporary, single-use object off to the garbage collector.

const hello = "Hello!" // variable bound to string primitive
hello.shout = hello.valueOf().toUpperCase().concat("!!!!!") // create a String wrapper object and add a shout method, which is evaluated immediately and is then sent to the garbage collector
// "HELLO!!!!!"
hello.shout // creates a NEW String wrapper object, where shout doesn't exist, so returns undefined 

Alas, poor String! I knew him.

You should also never ever EVER (probably) use these constructors in real-life, as even though it might make sense that creating these objects manually - especially for repetitive loops and the like - would be more efficient, the reverse is actually true. Implementations of JavaScript have worked out how to use the single-use wrapped objects way more effectively, which is a peculiar mishmash of timesaving and functionality that makes JavaScript such a hot and cold pleasure to deal with.

const { performance } = require('perf_hooks') // only needed for Node

function test_string(str, desc) {
  const t0 = performance.now()
  for (let i = 0; i < 10000000; i++) {
    str.length
    str.toLowerCase()
  }
  const t1 = performance.now()
  console.log(`${desc} took ${(t1 - t0) / 1000} seconds`)
}

test_string("Hello", "Wrapped string primitive");
test_string(new String("Hello"), "Instantiated String object");

// Wrapped string primitive took 0.011956994999200105 seconds
// Instantiated String object took 1.9173262799978257 seconds

Let's put JavaScript to one side and take a look at some other implementations. How's our little foo doing over in the ancient zen gardens of Ruby?

foo = 'foo'
foo.class # String
foo.class.ancestors # [String, Comparable, Object, Kernel, BasicObject]

In Ruby, all data is represented by objects. This helps Ruby become an elegant object orientated language, and Ruby coders to easily unwind in the evening without having to decompress out these particular day terrors. "Ruby follows the influence of the Smalltalk language by giving methods and instance variables to all of its types", says the language itself.

By tracing the String object through its ancestors, we see that it - and every object in Ruby - inherits from BasicObject.

magic_number = 3
magic_number.class.ancestors # [Integer, Numeric, Comparable, Object, Kernel, BasicObject]

So, all data types in Ruby are objects, and there's no wrapping/autoboxing going on behind the scenes, and even the JavaScript-style concept of primitives is non-existent.

Because everything starts life as an object, this means you can reopen a class and add your own methods, which I am legally required to tell you can be pretty dangerous: you should always always always make sure you ensure your code is maintainable and sticking your snout into the standard library is a great way to cause a total meltdown in real life. With that said, it's still pretty cool.

class String
  def shout
    self.upcase << "!!!!!"
  end
end

"hello".shout
# "HELLO!!!!!"

Elsewhere, in more functional languages, the concepts of objects and classes is usually a distant memory - so accessing methods directly on your data types is a no-no. If we follow our venerable foo over to Elixir, we can see that instead of accessing properties and methods we're now passing our data types as parameters to functions in the String module.

foo = "foo"
is_bitstring(foo) # true
String.upcase(foo) # "FOO"
String.length(foo) # 3

This is just a brief stop in Elixir, but it's great to see that our foo is doing well, even though we can barely recognise them from those early JavaScript days - they just grow up so fast!

Posted on by:

martingaston profile

Martin Gaston

@martingaston

Polyglot developer taking the midnight train going anywhere

Discussion

markdown guide