The following explanation is from an old friend of the show, Will (also known as @beiju). Unbeknownst to us, he’s been following along with Programming By Stealth all this time and just popped his head up for the first time in quite a few years.
Will wrote the following in an email to Bart and me:
In recent episodes Allison has been audibly frustrated about objects. I noticed a disconnect in Allison’s understanding of objects that I think is causing all of this. I wrote up a short explanation (at least, it was short when I started) that I think will close the gap. I attached it as an HTML document, complete with imperfect syntax highlighting and tiny font to make you feel at home. It’s written in a conversational style talking directly to Allison. I start with restating things that I think you (Allison) are already comfortable with and take a series of small steps, each with some code examples that you can use to prove that I’m telling the truth, until I arrive at the link that Allison’s missing.
I liked it enough I wanted to make sure it was shared with the entire Programming By Stealth family. So here is Will’s fantastic explanation of objects.
I’m going to attempt to reconcile the two correct definitions of a JavaScript object that we’ve heard: “a collection of name-value pairs” and “anything that isn’t a boolean, a number, or a string”. I’ll start with the one I think you’re more comfortable with — name-value pairs — and take small steps until we get to the other one.
So, an object is a collection of key-value pairs. The first thing to look at is, how do we make one? There are a few ways (you may not believe that yet, but I will convince you), but let’s start with the one we are more familiar with: the object literal. “Object literal” is just the official name for this syntax:
const myObj = {
"some key": "some value",
"some other key!": true,
"3rd key": 42
}
The keys are always strings (always, for any object), and the values can be anything. JavaScript also gives us a shortcut: if the keys follow all the rules for a valid variable name, we can leave out the quotes. Here’s almost the same object with the keys modified so they’re valid variable names.
const myObj = {
someKey: "some value",
someOtherKey: true,
key3: 42
}
I had to get rid of all the spaces and the exclamation mark, because they’re not allowed in variable names. I also had to move the digit 3 to the end of the third key, because variable names can’t start with numbers.
So now we have an object. An object made this way, by specifically listing out its names and values inside squiggly brackets, is often called a plain object. I’ll show you what’s plain about it when I talk about other objects.
Now, what are the things we can do with an object? First, we can get the value associated with a particular name. We do that with the square bracket operator, like this:
const theValue = myObject["some name"]
This might be the first sticking point, because you’re used to using the square brackets with arrays, to ask the question “what value is at this position in the array?”. Your understanding is correct, but not complete. Square brackets immediately after a variable name will get a specific value out of any collection of values. With arrays, the way we identify a specific value is by its position, so we put the position inside the square brackets. But with objects, we identify a specific value by its name, so we put the name inside the square brackets. Same concept, but applied in a different context.
As an aside, you don’t need to put a literal string (a string inside quote marks) in the brackets. You can put anything there that Javascript can turn into a string. myObject["some name"]
gets the value associated with the name “some name”, just like myArray[0]
gets the value at position 0. But you can put a variable in there, too: myObject[someVariable]
gets the value associated with whatever name is stored in the variable someVariable
, just like myArray[i]
gets the value at whatever position is stored in the variable i
. And myObject[stringOne + stringTwo]
will first figure out what stringOne + stringTwo
means, then get the value associated with whatever name that turns out to be, just like myArray[i + 1]
first figures out what the value of i + 1
and then gets the value at whatever position that turns out to be. That’s not really necessary to explain objects but it is something that I noticed Bart didn’t call attention to, so I thought it was worth saying.
Anyway, back to accessing name-value pairs within objects. Javascript once again gives you a shortcut. If you know the actual name you want, and it follows the rules for variable names, you can use the dot syntax instead of square brackets:
const theValue = myObject.someName
That is exactly equivalent to this:
const theValue = myObject["someName"]
The only advantage is that at looks prettier and it’s less typing. The disadvantage is that you can’t use it with a name that doesn’t follow those rules, or a name that’s stored in a variable.
You can do other things with an object too, which I don’t remember whether Bart has introduced yet. You can modify it, and the way you do that is very similar to getting values out of it. You just put the object name and attached square brackets on the other side of the equals sign:
myObject["someName"] = "a new value"
If there’s already a value associated with the name “someName” in the object, this will overwrite that value with the string “a new value”. If there isn’t, it adds a new pair with name “someName” and value “a new value”.
I went over all that because I’m about to show you some other ways to make objects. I expect that you might not believe me at first, but I will prove that those are also objects by showing that you that everything I just talked about works with them, too.
Here is a second way to create an object:
const myCow = new Cow()
For this example, I’m using the Cow class that was part of PBS 47 and its associated challenge, the one where we made a farm. I claim that this line of code creates a new object, using the same definition of an object as before: “a collection of key-value pairs”.
Here’s where I prove it. Open the PBS 47 challenge solution in your favorite web browser (the zip file is in PBS 48). Open the console and make yourself a new animal using the code snippet above. With myCow
, you can:
- Get the value associated with a name.
Cow
s (and allAnimal
s) all have a value associated with the name_icon
(with underscore), so trymyCow["_icon"]
. You get a value — it’s an emoji cow. That name also follows the rules for Javascript variable names, so we can equivalently saymyCow._icon
. And, lo and behold, we get the same thing. - Change the value associated with a name. Try
myCow._icon = 12
, then runmyCow._icon
again. You just changed it to 12. You could do the exact same thing withmyCow["_icon"] = 12
. - Add a new name-value pair. First, run
myCow.newName
to demonstrate that it’s not there (which Javascript indicates with the valueundefined
). Then domyCow.newName = "boogers"
, and then get the value again. NowmyCow.newName
should give back"boogers"
. Once again, you could do all that with the square bracket syntax, too. You can even mix and match dots and square brackets!
Hopefully that convinces you that myCow
is an object. It doesn’t really explain why it’s an object, though. For that, think back to when Bart introduced prototypes (PBS 17). The problem to be solved was that we wanted to make a bunch of similar objects. They would contain the same set of names, but different values. We also wanted to bundle in a bunch of functions that would do useful things to these objects. Prototypes (which we now also call classes) were a convenient way to build those objects, but at the core, they’re still just a collection of name-value pairs. Even the functions that we bundled in (called “instance functions”), we bundled as more name-value pairs. You can see this by looking for the functions on myCow
. Look at myCow.getProduce
in the console — it’s a function. Add some parentheses on the end to invoke the function, just like how you invoke any function, and you get myCow.getProduce()
.
Look familiar? That’s how we’ve been calling functions from our classes the whole time, and it turns out all we were doing was using the dot syntax from objects! We can equivalently say myCow["getProduce"]()
, and it does the exact same thing. It’s the same story inside the instance functions, too. We’ve been doing things like this._icon
, to get an animal’s icon, or this._grid
to get the grid of cells in a cellular automation. That’s just the dot syntax to access the properties of the object. (Remember that inside instance functions, this
is set to the specific object that the function was called from.) We could equivalently say this["_icon"]
or this["_grid"]
.
At this point, I’m hoping that something just clicked into place. Hopefully you now believe that class instances are objects the same way plain objects are. Objects made by listing name-value pairs inside squiggly brackets are “plain”. Objects made from a prototype are not.
Now let’s look at another syntax for creating objects:
const myArray = [false, 1, "two"]
I’m sure you agree that this makes an array. And arrays are objects, therefore this makes an object. I’m not sure you agree with this one yet, but I will try to prove it.
As Bart has said, the names of all the values in an array are their positions. In the above example, 0
and false
is one name-value pair, 1
and 1
is another, and 2
and "two"
is a third. You can access the values using those names: myArray[0]
gets the value false
, etc. The only reason you can’t use the dot syntax here is that JavaScript variables are not allowed to start with a number. If they were, then myArray.0
would be equivalent to myArray[0]
.
Arrays also have a length, which is just another name-value pair. myArray["length"]
is exactly equivalent to myArray.length
. length
does have some extra behavior: it updates itself whenever you change the size of the array, and it throws an error if you try to set it to a value that isn’t a number. We could write a class that does the same thing if we wanted to, but it’s very uncommon to need so we haven’t learned how to do it.
In addition to data, arrays also have functions. We’ve seen instance functions, like someArray.forEach()
and someArray.sort()
, and also static functions like Array.from()
. In summary: An array behaves just like an object that was created from a prototype, because it is an object that was created from a prototype. An array is just an object with the Array prototype. Behind the scenes, whenever you put a bunch of values in square brackets, JavaScript is doing new Array()
and then adding all your values to the new Array object. You could build the array that way yourself, if you wanted. This:
const myArray = new Array()
myArray[0] = false
myArray[1] = 1
myArray[2] = "two"
Is exactly equivalent to this:
const myArray = [false, 1, "two"]
We’re nearing the end of the list so I’ll go quicker. Another way to make an object is like this:
function myFunc(a, b) {
return a + b;
}
Yes, functions are objects too! That might be even more surprising than arrays, since normally when we use functions there isn’t a name-value pair in sight! That doesn’t mean they aren’t there, though. Run the above snippet in the browser console, then run myFunc.name
— you get "myFunc"
! Functions also have a length: myFunc.length
is the number of arguments the function has, in this case 2. They’ve got instance functions, too: myFunc.call
is a function that we learned about in PBS 48. Functions are starting to look an awful lot like objects with prototypes, because they are. In this case the prototype is, unsurprisingly, Function
.
The fact that you can call a function with ()
is a little bit magic, but not as much as you might expect. One of the name-value pairs on a Function
instance has a very special name, and the associated value is the code that should run. Any object that has a value associated with that very special name can be called with ()
. The magic is that the special name is some unspecified string that cannot ever be typed into a computer, so it’s impossible for you to make an object with that name. Only JavaScript itself has the power to make an object with that special name.
There is at least one more example of a prototype that’s given special syntax (a regular expression, for example, which you make with const myRegExp = /stuff/
), but the idea is always the same: it’s just some special syntax for making an object with a specific prototype (for regular expressions, that’s RegExp
).
Before I finish, I want to fix one small white lie I told. I said that a plain object was one that you made with the squiggly brackets syntax, which was true. I also said that an object made from a prototype was not, which isn’t quite true. The very first way I introduced for making an object was the squiggly-bracket syntax. Well, the squiggly-bracket syntax is just another special syntax for making an object with a specific prototype! Just like the square brackets that make an array, or the special syntax for making a function. The squiggly-bracket syntax makes objects with the prototype Object
. Like with arrays, you could make them yourself by adding name-value pairs one by one:
const myObj = new Object()
myObj["someKey"] = "some value"
myObj["someOtherKey"] = true
myObj["key3"] = 42
Or equivalently, using the dot syntax:
const myObj = new Object()
myObj.someKey = "some value"
myObj.someOtherKey = true
myObj.key3 = 42
The Object
prototype is the most basic prototype JavaScript has. It doesn’t add any data properties, but it does add a small number of functions. Try making a plain object, then calling toString()
on it. The result isn’t very helpful, but you do get a result, so that proves that the function exists.
Now we know that the squiggly brackets for objects, the square brackets for arrays, the function
keyword for functions, and the slashes for regular expressions are all just special syntaxes that make it more convenient to make objects with some specific prototypes. That means that, really, there are just two ways to make objects: new
, and one of those special syntaxes that just hides a new
under the hood*. Any way of creating anything, unless it’s a boolean, number, or string, is just creating an object under the hood. Or, in other words, anything that isn’t a boolean, number, or string is an object. As promised, we have now hopefully reconciled the two definitions of an object that we started with. Anything in JavaScript that isn’t a boolean, number, or string is a collection of name-value pairs, aka an object. Some objects are plain, others are arrays, others are functions, and others are some other type of object — it all depends on the object’s prototype.
(* There are technically two other ways to make an object. Object.create()
is a built-in function that creates an object in advanced ways. Also, when you use the square brackets or dot syntax a number, boolean, or string, as if it were an object, it gets temporarily converted to an object with prototype Number
, Boolean
, or String
respectively.)
I can now tell you the technical definition of a plain object: A plain object is an object with the prototype Object
. There are three ways to get one of those: the squiggly-brackets syntax we are used to, new Object()
, and a static function we haven’t learned about called Object.create()
that lets you do some advanced stuff. But 99.99% of the time there’s no reason not to use squiggly brackets, so a very easy definition that’s almost always true is plain objects are objects created using squiggly brackets.
JavaScript programmers very often just say “object” when we mean “plain object”. This came up in PBS 51 when you were reviewing the previous challenge. Bart said to make the fifth argument expect an object. You made it expect an array, then he said that was wrong because he wanted an object. But, as you pointed out, an array is an object! He said it was wrong because he wanted a plain object. Who is technically right depends on how you want to read the challenge description. Bart does mention earlier that it’s going to be a plain object, but the sentence that actually tells you what to do just says to make “a single optional object named opts
”, which doesn’t mention plain-ness. I encourage you to ask Bart often whether he means any object or specifically a plain object, until you build enough experience to figure it out from context.