Learn why the relationship between primitives and objects is complicated...especially when dealing with the built-in types 🤗
In the earlier Strings tutorial and less so in the Of Pizza, Types, Primitives, and Objects tutorial, we got a sneak peek at something that is probably pretty confusing. I've stated many times that primitives are very plain and simple. Unlike Objects, they don't contain properties that allow you to fiddle with their values in interesting (or boring) ways. Yet, as clearly demonstrated by all the stuff we can do with strings, our primitives seem to have a mysterious dark side to them:
let greeting = "Hi, everybody!!!"; let shout = greeting.toUpperCase(); //where did toUpperCase come from?
As we can see from this brief snippet, our greeting variable, which stores a primitive value in the form of text, seems to have access to the toUpperCase method. How is this even possible? Where did that method come from? Why are we here? Answers to confusing existential questions like this will make up the bulk of what you will see in this page. Also, I apologize for writing that previous sentence in passive voice. Happen again it won't.
Because of how fun and playful they are (kind of like a Golden Retriever!), it's easy to pick on strings as the main perpetrator of this primitive/Object confusion. As it turns out, many of the built-in primitive types are involved in this racket as well. Below is a table of some popular built-in Object types with most of the guilty parties (Symbol and BigInt will be sitting this one out) that also exist as primitives highlighted:
|Type||What it does|
|Array||helps store, retrieve, and manipulate a collection of data|
|Boolean||acts as a wrapper around the boolean primitive; still very much in love with true and false|
|Date||allows you to more easily represent and work with dates|
|Function||allows you to invoke some code among other esoteric things|
|Math||the nerdy one in the group that helps you better work with numbers|
|Number||acts as a wrapper around the number primitive|
|RegExp||provides a lot of functionality for matching patterns in text|
|String||acts as a wrapper around the string primitive|
Whenever we are working with boolean, number, or string primitives, we have access to properties their Object equivalent exposes. In the following sections, you'll see what exactly is going on.
Just as you were taught by your parents growing up, we typically use a string in the literal form:
let primitiveText = "Homer Simpson";
As we saw in the table earlier, strings also have the ability to be used as objects. There are several ways to create a new object, but the most common way to create an object for a built-in type like our string is to use the new keyword followed by the word String:
let name = new String("Homer Simpson");
The String in this case isn't just any normal word. It represents what is known as a constructor function whose sole purpose is to be used for creating objects. Just like there are several ways to create objects, there are several ways to create String objects as well. The way I see it, knowing about one way that you really shouldn't be creating them with is enough.
Anyway, the main difference between the primitive and object forms of a string is the sheer amount of additional baggage the object form carries with it. Let's bring our silly visualizations back. Our primitiveText variable and its baggage looks as follows:
There really isn't much there. Now, don't let the next part scare you, but if we had to visualize our String object called name, here is what that would look like:
You have your name variable containing a pointer to the text, Homer Simpson. You also have all of the various properties and methods that go with the String object - things you may have used like indexOf, toUpperCase, and so on. You'll get a massive overview of what exactly this diagram represents when we look at Objects in greater detail, so don't worry yourself too much about what you see here. Just know that the object form of any of the primitives carries with it a lot of functionality.
let game = "Dragon Age: Origins";
The game variable is very clearly a string primitive that is assigned to some literal text. If I want to access the length of this text, we would do something as follows:
let game = "Dragon Age: Origins"; console.log(game.length);
This transformation only happens for primitives. If we ever explicitly create a String object, then what we create is permanently kept as an object. Let's say we have the following:
let gameObject = new String("Dragon Age:Origins");
In this case, our gameObject variable very clearly points to something whose type is Object. This variable will continue to point to an Object type unless you modify the string or do something else that causes the reference to be changed. The primitive morphing into an object and then morphing back into a primitive is something unique to primitives. Your objects don't partake in such tomfoolery.
You can easily verify everything I've said by examining the type of your data. That is done by using the typeof keyword. Here is an example of me using it to confirm everything I've just told you about:
let game = "Dragon Age: Origins"; console.log("Length is: " + game.length); let gameObject = new String("Dragon Age:Origins"); console.log(typeof game); // string console.log(typeof game.length); // number console.log(typeof gameObject); // object
Now, aren't you glad you learned all this?
Hopefully this brief explanation helps you to reconcile why our primitives behave like objects when they need to. At this point, you might have a different question around why anybody would have designed a language that does something this bizarre. After all, if a primitive turns into an object when it needs to do something useful, why not just stay an object always? The answer has to do with memory consumption.