Review
Last week, we looked at operators in-depth, and spoke extensively about type-coercion.
Let's remember a few of the rules that our interpreter applies when we give operators different types!
Evaluate each of the following expressions. Determine the type of the final output.
// typeof [15, RL] // + [13, LR] typeof "lima" + " bean"
// * [15, LR] // +, - [13, LR] "5" * "5" - "5" + "5"
Now let's try a few harder ones concerting precedence and type coercion! Use the operators at-a-glance below (with their precedence levels and associativity listed) and write out each step of the evaluated expression.
() [19] |
. [18, LR] |
!, typeof [15, RL] |
* [15, LR] |
+, - [13, LR] |
>= [11, LR] |
=== [10, LR] |
&& [6, LR] |
|| [5, LR] |
// #1 let x = "123"; !"2" + 1 * 3 - x.charAt(1) + x.charAt(2)
// #2 (typeof(5 - "5" || 5 >= 5)).charAt(0) === "b"
// #3 let x; 0 || 5 === "5" && "0" || x
// #4
!("1" !== 1 > 1) + !"1" - (null || 2 && 3)
Objects
Returning to our peaceful grassy knoll and sun as illustrated in p5.js, suppose we want to add a little life to the scene... I give you, the disco cloud:
See the Pen Sun with Disco Cloud by Andrew Forney (@forns) on CodePen.
What about the implementation of the disco cloud is particularly unsustainable / will not scale well (hint: what happens when we want to add more clouds?)
It will become cumbersome to maintain the individual variables cloud1X, cloud1R, ..., cloud200X, cloud200R, ..., etc. and it is difficult to organize properties belonging to particular clouds.
Intro to Objects
In the first two weeks, we learned about all of JavaScript's primitive types: numbers, strings, booleans, null, and undefined.
We left Objects off until we could focus on them specifically, and that's just what we'll do today!
Objects are any value in JavaScript that are not numbers, strings, booleans, null, or undefined. Objects are useful for grouping variables into single "entities" with various attributes.
Objects have properties, which are labels given to its attributes that can be strings, positive integers, or named like variables.
Each property has a corresponding value, which can be any primitive type (like a string or number), or another object!
But enough definitions, let's take a look at some objects!
The object creation syntax is as follows, with comma-separated key-value pairs and a colon separating each property/key from its value:
{prop1: val1, prop2: val2, prop3: val3} OR
{"prop1": val1, "prop2": val2, "prop3": val3}
// The "empty" object
let soEmpty = {};
// An object with 1 property
let person = {
name: "Andrew"
};
// An object with 2 properties
let zombie = {
name: "Brain B. Goode",
peepsEaten: 10
};
// An object with 3 properties,
// one of which is another object
let survivor = {
name: "Rick Grimes",
walkersKilled: 137,
weapon: {
name: "Colt Python",
ammo: 6
}
};
So why do we have objects anyways?
They help us organize our data! Who would want to type and keep track of variables named: survivor1Name = "Rick Grimes", survivor1WeaponName = "Colt Python", ... etc?!
Later in the course, we'll look at means of using objects to denote prototypical data organizations, that we can then use to be even more organized!
More on that later, first let's focus on the basic mechanics of objects.
Manipulating Properties
So we've got these great objects... how to we talk about the data collected in them? Better yet, how do we modify it?
There are two ways to refer to an object's properties:
The dot notation
obj.prop: used to access known / non-varying property names.The bracket notation
obj["prop"]: used to access variable property names that may not be known until the script is executed. Also, necessary for accessing string property names with characters other than numbers, letters, or underscores.
Note: in the dot notation, the property being accessed is NOT a string. In the bracket notation, the property is a string.
Accessing Properties
So now that we know the syntax of how to access a property, what can we do with it?
Essentially anything that we could with any other value! Let's try a few things:
let survivor = {
name: "Rick Grimes",
walkersKilled: 137,
weapon: {
name: "Colt Python",
ammo: 6
}
};
// Basic access
survivor.name => "Rick Grimes"
survivor["name"] => "Rick Grimes"
survivor.walkersKilled => 137
survivor.weapon => {name: "Colt Python", ammo: 6}
// Accessing with variable property
let prop = "name";
survivor[prop] => "Rick Grimes"
// Using methods and operators
survivor.name.indexOf("Grimes") => 5
survivor.walkersKilled + 1 => 173
Now notice something else: the weapon property of our survivor object is itself an object!
Objects that are the values of properties within objects referred to as nested objects.
How do we access its properties? Easy!
// (using the survivor object above) // Accessing nested properties survivor.weapon.name => "Colt Python" survivor.weapon["name"] => "Colt Python" survivor["weapon"].name => "Colt Python" survivor.weapon.name.toUpperCase() => "COLT PYTHON"
You should also be aware of a very common behavior when you're accessing object properties, specifically, ones that don't exist:
Attempting to access a property that does not exist in an object will return undefined.
Attempting to access any property of something undefined will yield a syntax error.
// (using the survivor object above) survivor.carrrl => undefined survivor.carrrl.hat // [X] Syntax error!
Note: want to avoid a syntax error when a nested object *might* be undefined? Use logical AND (&&) to ensure that a property is defined; it will not evaluate its rvalue if its lvalue is falsy.
// (using the survivor object above) survivor.carrrl && survivor.carrrl.hat => undefined
Notice that survivor.carrrl.hat would normally give a syntax error, but because we first checked whether or not survivor.carrrl was defined (it isn't), the logical AND stops there and
returns undefined.
Setting Properties
Now the same way that we might change or declare new variables, we can also do for object properties.
Using the assignment operator (=), we can create or alter any properties of an object.
// (using the survivor object above) // Changing a property survivor.name = "Daryl Dixon"; survivor.walkersKilled = 142; // Adding a property survivor.gender = "male";
Deleting Properties
Suppose we don't want our object to have a property stored any longer. We can nuke it!
The delete operator removes a property from an object entirely.
let survivor = {
name: "Rick Grimes",
walkersKilled: 137,
weapon: {
name: "Colt Python",
ammo: 6
}
};
// Delete the weapon
delete survivor.weapon;
// Survivor is now:
// {name: "Rick Grimes", walkersKilled: 137}
Replace the cloudX, cloudR, cloudG, cloudB variables in our Disco Cloud example above with a cloud object and corresponding X, R, G, B properties. Make the necessary changes in drawCloud to account for this change.
Object References
Try to create another cloud from our example above by setting cloud2 = cloud and then copying the drawCloud
function (of course, not good style, but we'll look at solutions to keep our code DRY later). What is peculiar about the resulting behavior?
Earlier, we noted that objects behave a little bit differently than the primitive types with regards to how they are stored in variables and properties; let's look more at that now.
Primitive values are stored in variables themselves, whereas the "value" of an object is not the object itself, but a reference or pointer to the object.
That sounds a bit confusing at first, but a few pictures will help us understand...
Drawing Objects
The best way to understand the mechanics of objects are to draw pictures of them.
When we draw an object, we represent properties like their own "variables" boxed inside the containing object.
When we draw a variable instantiated to an object, we represent the variable as containing a pointer to that object, rather than the object itself.
Implications for Assignment
So if objects aren't stored inside of variables, then what happens when we try to assign other variables to variables containing an object reference?
Assigning a variable to the value of another variable containing an object pointer simply replicates the pointer without replicating the object.
So above, this means that any time I manipulate or access the properties of zomA, I'm really talking about the same object as zomB!
Note: this behavior of copying the pointer is only manifest in objects, not primitive types!
Recall that primitive values are copied directly without any of this pointer business, since their values live "within the variables themselves."
So, for the assignment operator (=), we can think of primitives as copying the rvalue into the lvalue directly, and for objects as copying the pointer to the object in the rvalue into the lvalue.
What will the following script alert? Draw each variable and make appropriate changes to your picture as assignments are made.
let zomB = {
name: "Zom B.",
peepsEaten: 5
},
zomA = zomB,
oldHome = "Atlanta",
newHome = oldHome;
zomA.name = "Zom A.";
alert(zomB.name);
newHome = "BRAIIINS";
alert(oldHome);
Object Creation
So what's happening when I use that curly bracket syntax? A couple definitions will help us understand:
An object literal is any expression denoting a new object; we've learned one such expression using the curly-brackets: { ... }
Any time we evaluate an object literal, a new object is created in memory.
The above point is the second major difference between objects and primitive types.
Whereas primitive literals don't create new entities in memory any time they're evaluated, object literals do.
So consider the following source code where we create two new objects in memory, even though they look identical:
Objects & Operators
Because of the above behavior, objects work a little differently than you might anticipate with some operators.
The strict equivalence operator (===) returns true if and only if the lvalue and rvalue are the same object in memory (i.e., the pointers point to the same object), regardless of the operands' properties or values.
This means that no two object literals can be strictly equivalent (===), since every evaluation of an object literal creates a new object in memory.
So when can strict equivalence return true for two objects? Let's take a look... with pictures!
p1 === p2 => false
p3 === p2 => false
p1 === p3 => true
{x:1} === {x:1} => false
All objects are considered truthy (even the empty object).
Boolean({}) => true
! {x: 1} => false
Fun with References
As if that last section wasn't complex enough, let's add some more layers of complexity!
Nested Objects & References
From the last section, we learned two important facts: (1) object literal evaluation creates a new object, and (2) assigning a variable to an object copies the reference to that object into the variable.
Let's see how these facts play with nested objects!
First off, let's create a nested object and see how to draw it:
Now, suppose I create another reference to this nested object 0_o
What happens to p1 if I assign w1.name = "really sharp stick!";
Self-reference
Self-reference: (see definition of self-reference)
OK that was a bad joke, but wasn't it worth it?
Self-reference is exactly what it sounds like: an object has a property that, shockingly, contains a pointer to itself! :O
Let's see what that looks like:
Self-reference can lead to hilarious, but still legal syntax.
// using definition of p1 above: alert(p1.woah.id); alert(p1.woah.woah.woah.woah.id);
We'll look at the uses of self-reference later, but you should consider it now as a legal operation given the rules we've learned that govern objects!
Consider the following objects and (1) draw their pictorial representation and then (2) indicate what gets alerted at the end!
let obj1 = {
p1: {
p1: 1,
p2: 2
}
},
obj2 = {
p1: {
p1: 3,
p2: 4
},
p2: obj1
},
p1 = "p2";
obj1.p2 = obj1;
// Perhaps the most sinister example I've ever created
alert(obj1.p2.p1.p2);
alert(obj2.p1[p1]);
alert(obj2[p1][p1].p1["p1"]);