objects
What I learned, what I’m still figuring out, and where I might go next.
Objects
JavaScript objects are an almost weirdly versatile collection type. Object literals are often used to store data in key-value pairs.
const apple = {
name: "Apple",
radius: 2,
color: "red",
};
You can access properties stored on an object using the . operator:
console.log(apple.name); // prints "Apple"
console.log(apple.radius); // prints "2"
console.log(apple.color); // prints "red"
No Colon
The key: value syntax is the normal way to create key-value pairs in an object, but if you want a key to have the same name as an existing variable, you can omit the colon and the value.
const radius = 2;
const color = "red";
const apple = {
radius, // same as radius: radius
color: color, // set explicitly for demonstration
};
Updating Properties
You can update and add new keys (“property” and “key” can be used interchangeably) to an existing object using the . operator. If it exists, it’s updated; if it doesn’t, it’s created as a new property:
const apple = {
name: "Apple",
radius: 2,
color: "red",
};
apple.numSeeds = 3; // new property
apple.color = "green"; // update property
// {"name":"Apple","radius":2,"color":"green","numSeeds":3}
Nesting Properties
Objects can contain other objects. Here we’ve nested two object literals within the tournament object:
const tournament = {
referee: {
name: "Sally",
age: 25,
},
prize: {
units: "dollars",
value: 100,
},
};
We can access nested properties the same way by chaining: tournament.referee.name
console.log(tournament.referee.name); // Sally
console.log(tournament.prize.value); // 100
Optional Chaining
When using the normal . operator, if the object on the left side of the . is null or undefined, you’ll get a TypeError at runtime. Thankfully, JavaScript has recently added a new operator to make dealing with this headache easier, the optional chaining operator: ?.
const tournament = {
prize: {
units: "dollars",
value: 100,
},
};
const h = tournament.referee.height;
// TypeError: Cannot read properties of undefined (reading 'height')
So, if you’re not sure whether the referee property exists (maybe it was sent to us over the network) we can use the optional chaining operator to avoid the error:
const tournament = {
prize: {
units: "dollars",
value: 100,
},
};
const h = tournament.referee?.height;
// h is simply undefined, no error is thrown
Object Methods
JavaScript objects can have methods, just like classes in Python or structs in Go. Objects are interesting in JavaScript because they play the role of dictionaries and classes in other languages.
Methods are functions that are defined on an object. They can access and change the properties of the object in question. In the context of an object method, the this keyword refers to the object itself, like self in Python.
const person = { firstName: “John”, lastName: “Smith”, getFullName() { return this.firstName + ” ” + this.lastName; }, };
console.log(person.getFullName()); // John Smith
Methods Mutate
Methods can change the properties of their objects as well:
const tree = {
height: 256,
color: "green",
cut() {
this.height /= 2;
},
};
tree.cut();
console.log(tree.height);
// prints 128
tree.cut();
console.log(tree.height);
// prints 64
Initializing Props
If a property (key) doesn’t exist when we try to access it with the . operator, we’ll just get undefined. One way to check for this is by using the ! (not) operator because undefined is “falsy” (meaning it evaluates to false in a boolean context). The syntax is simple:
const balances = {
lane: 100,
breanna: 150,
john: 200,
};
// if bob doesn't have a balance yet
// create a new prop for him
// set to 0
if (!balances.bob) {
balances.bob = 0;
}
Strings as Keys
Accessing a property like desk.height is great when the name of the prop is static, meaning you know what it is before runtime. But what if the key is dynamic? Like, what if the user enters a string and you need to use that as the lookup key?
Bracket notation solves this.
const desk = {
wood: "maple",
width: 100,
};
console.log(desk.wood);
// prints "maple"
console.log(desk["wood"]);
// also prints "maple"
const key = "wood";
console.log(desk[key]);
// also prints "maple"
This
this refers to the context where a piece of code is executed.
Global Context
// in a browser
console.log(this);
// Window { ... }
// in Node.js
console.log(this);
// {}
Strict Mode
In strict mode, this is undefined in the global scope in both the browser and Node.js.
"use strict";
console.log(typeof this);
// undefined
Method Context
Inside a standard method or a constructor, this refers to the object the method is called on.
const myObject = {
message: "Hello, World!",
myMethod() {
console.log(this);
console.log(this.message);
},
};
myObject.myMethod();
// {"message":"Hello, World!"}
// Hello, World!
Arrow Functions
Fat arrow functions, or “arrow functions” are another way to define functions in JavaScript. Arrow functions are newer than the function keyword, however, unlike the let/const syntax, arrow functions are sometimes better, not always better.
// declaring a function without a variable
function add(x, y) {
return x + y;
}
// declaring a function with a variable
const add = function (x, y) {
return x + y;
};
// using the fat arrow syntax
const add = (x, y) => {
return x + y;
};
One reason to choose an arrow function over a regular function or method is to preserve the this context. It’s particularly useful when working with objects. To be fair, in a simple object like this, the non-arrow method makes perfect sense:
const author = {
firstName: "Jay",
lastName: "Nejati",
getName() {
return `${this.firstName} ${this.lastName}`;
},
};
console.log(author.getName());
// Prints: Jay Nejati
With a fat-arrow function, the this keyword refers to the same context as its parent. In essence, fat arrow functions “preserve” the this context. That’s why this this.firstName and this.lastName are undefined in this example:
const author = {
firstName: "Lane",
lastName: "Wagner",
getName: () => {
return `${this.firstName} ${this.lastName}`;
},
};
console.log(author.getName());
// Prints: undefined undefined
// because `this` still refers to the global object
// and `firstName` and `lastName` are not defined globally
Developers working in some front-end JavaScript frameworks (like React or Vue) tend to use fat arrow functions often. The this context can contain a ton of component-wide state, and it needs to be preserved throughout nested function calls, so fat arrows make the code easier to read and write.
Spread Syntax
JavaScript has a really nifty spread syntax for moving groups of object properties around. It’s a great way to copy objects and merge object properties.
const engineering_dept = {
lane: "grand magus",
hunter: "software engineer",
allan: "software engineer",
matt: "software engineer",
dan: "software engineer",
waseem: "software engineer",
};
const video_dept = {
stass: "video producer",
alex: "video producer",
};
const all_employees = { ...engineering_dept, ...video_dept };
/*
{
lane: 'grand magus',
hunter: 'software engineer',
allan: 'software engineer',
matt: 'software engineer',
dan: 'software engineer',
waseem: 'software engineer',
stass: 'video producer',
alex: 'video producer'
}
*/
The spread syntax shallow-copies the properties of the objects you’re spreading. If properties have the same name, the last (right-most) object’s property will overwrite the previous ones.
const engineering_dept = {
lane: "software engineer",
hunter: "software engineer",
};
const video_dept = {
lane: "cringe youtuber",
alex: "video producer",
};
const all_employees = { ...engineering_dept, ...video_dept };
/*
{
lane: 'cringe youtuber',
hunter: 'software engineer',
alex: 'video producer'
}
*/
Return Objects
In JavaScript, we can only return a single value from a function. So, when you want to return multiple values, you just return an object that contains those values.
function doAllTheMath(x, y) {
const sum = x + y;
const difference = x - y;
const product = x * y;
const quotient = x / y;
return {
sum,
difference,
product,
quotient,
};
}
const results = doAllTheMath(10, 5);
console.log(results.sum);
// 15
console.log(results.difference);
// 5
console.log(results.product);
// 50
console.log(results.quotient);
// 2
Destructuring
It’s admittedly annoying to have to get the return values from an object by using the . operator. The destructuring assignment lets us unpack object properties easily.
Instead of this:
const apple = {
radius: 2,
color: "red",
};
const radius = apple.radius;
const color = apple.color;
We can do this:
const apple = {
radius: 2,
color: "red",
};
const { radius, color } = apple;
It can be used to unpack function return values:
function getApple() {
const apple = {
radius: 2,
color: "red",
};
return apple;
}
const { radius, color } = getApple();
console.log(radius); // 2
console.log(color); // red
Destructuring also works in function parameters, which means that if you write a function that takes an object as an argument, you can unpack the object’s properties in function definition.
Instead of this:
function eatApple(apple) {
console.log(`ate a ${apple.color} apple with a radius of ${apple.radius}`);
}
We can do this:
function eatApple({ radius, color }) {
console.log(`ate a ${color} apple with a radius of ${radius}`);
}
Not Bound
Methods in JavaScript are not bound to their object by default (as they are in languages like Python and Go). So if you use a method as a “callback” function, you may run into issues with the this keyword:
const user = {
name: "Jay",
sayHi() {
console.log(`Hi, my name is ${this.name}`);
},
};
user.sayHi();
// Hi, my name is Jay
const sayHi = user.sayHi;
sayHi();
// TypeError: Cannot read properties of undefined (reading 'name')
This happens a lot when passing a method as a callback function to another function:
const user = {
firstName: "Jay",
lastName: "Nejati",
getFullName() {
return `${this.firstName} ${this.lastName}`;
},
};
function getGreeting(introduction, nameCallback) {
return `${introduction}, ${nameCallback()}`;
}
console.log(user.getFullName());
// Jay Nejati
console.log(getGreeting("Hello", user.getFullName));
// TypeError: Cannot read properties of undefined (reading 'firstName')