JavaScript Primer

--- views
Banner image for JavaScript Primer

01 String Interpolation#

In earlier versions of JavaScript, if we wanted to dynamically create strings, we needed to use the addition operator (+):

old-string-concatenation
const userName = 'Mai';
const dynamicString = 'hello ' + userName + '!';

This works alright, but it feels pretty clunky at times, and can lead to bugs (eg. forgetting the space in Hello ).

Modern JavaScript allows us to embed variables and other expressions right inside strings:

string-interpolation
const dynamicString = `hello ${userName}!`;

In order to use string interpolation, we need to use backticks (`).

Strings created with backticks are known as "template strings". For the most part, they function just like any other string, but they have this one super-power: they can embed dynamic segments.

We create a dynamic segment within our string by writing ${}. Anything placed between the squiggly brackets will be evaluated as a JavaScript expression.

This means that we can do fancy things like this:

template-string-expression
const age = 7;
console.log(`Next year, you'll be ${age + 1} years old.`);

This creates the string "Next year, you'll be 8 years old.".

02 Arrow Functions#

Historically, functions in JavaScript have been written using the function keyword:

traditional-function
function exclaim(string) {
  return string + '!';
}

In 2015, the language received an alternative syntax for creating functions: arrow functions. They look like this:

arrow-function
const exclaim = string => string + '!';

Arrow functions are inspired by lambda functions from other functional programming languages. Their main benefit is that they're much shorter and cleaner. Reducing "function clutter" may seem like an insignificant benefit, but it can really help improve readability when working with anonymous functions. For example:

arrow-function-example
const arr = ['hey', 'ho', 'let\'s go'];

// This:
arr
  .map(function(string) {
    return string + '!'
  })
  .join(' ');

// …Becomes this:
arr
  .map(string => string + '!')
  .join(' ');

Rules of arrow functions#

Arrow functions might seem straightforward at first glance, but there are a few "gotchas" to be aware of. It's super common for folks to get tripped up by some of these rules.

Short-form vs. long-form#

There are two types of arrow functions: short-form and long-form.

Here's the short-form:

short-form-arrow
const add1 = n => n + 1;

And here's the long-form:

long-form-arrow
const add1 = n => {
  return n + 1;
};

We opt into the long form by adding curly braces ({ }) around the function body.

The fundamental difference between the two forms is this:

  • The short-form function's body must be a single expression. That expression will be automatically returned.
  • The long-form function's body can contain a number of statements. We need to manually specify the return.

With the short-form, our function body consists of a single expression, and that expression will be returned. This is sometimes called an "implicit" return, because there's no return keyword.

When we add the squiggly brackets ({ }), we create a block of statements. We can put as many statements as we want in there. We need to specify what should be returned using a return statement.

Interestingly, if we try to add the return keyword to the short-form syntax, we'll get a syntax error:

syntax-error-example
const add1 = n => return n + 1;
// Uncaught SyntaxError: Unexpected token 'return'

Why is this an error? Well, return n + 1 is a statement, not an expression. The short form requires an expression. We're not allowed to put a statement there.

Parentheses instead of squigglies?

Sometimes, we'll see arrow functions written like this:

const shout = sentence => (
  sentence.toUpperCase()
);

What does it mean when we use parentheses (( )) instead of curly braces ({ })?

Parentheses can be added to help us with formatting. By adding parens, we're able to push the returned expression to a new line. And so, we're still using the short-form "implicit" return structure, but we're restructuring it to make it more readable.

To put it another way, these two functions are identical:

// On multiple lines, with parens:
const shoutWithParens = sentence => (
  sentence.toUpperCase()
);

// Or, in a single line without parens:
const shoutWithoutParens = sentence => sentence.toUpperCase();

Optional parameter parentheses#

If an arrow function takes a single parameter, the parentheses are optional:

optional-parentheses
// This is valid:
const logUser = user => {
  console.log(user);
}

// This is also valid:
const logUser = (user) => {
  console.log(user);
}

The parentheses are mandatory if we have more than 1 parameter:

multiple-parameters
const updateUser = (user, properties, isAdmin) => {
  if (!isAdmin) {
    throw new Error('Not authorized')
  }

  user.setProperties(properties);
}

The parentheses are also mandatory if we have no parameters:

no-parameters
const sayHello = () => console.log('Hello!')

Implicitly returning objects#

Let's suppose we have a function which returns an object:

function-returning-object
function makeObject() {
  return {
    hi: 5,
  };
}

Something sorta funny happens when we try and convert it to a short-form arrow function:

problematic-arrow-object
const makeObject = () => { hi: 5 };

There are two ways we could interpret this code:

  1. A short-form arrow function that returns an object, { hi: 5 }
  2. A long-form arrow function with a single statement, hi: 5

The problem is that curly braces ({}) serve two purposes in JavaScript: they're used for object notation, but they're also used to create blocks, like in if statements.

When curly braces follow an arrow (=>), the JS engine assumes we're creating a new block, and so we'll get a syntax error, since hi: 5 is not a valid JS statement.

If we want to implicitly return an object, we need to wrap it in parentheses:

correct-arrow-object
const makeObject = () => ({ hi: 5 });

In JavaScript, parentheses can be added around any expression, to change its evaluation order. In this case, we don't care about evaluation order, we just need to make it clear that we're trying to pass an expression.

Similarly, it's common to wrap the short-form expression in parentheses when it's too long to fit on a single line:

multiline-arrow-expression
const matchedItem = items.find(item => (
  item.color === 'red' && item.size === 'large'
));

We can wrap any expression in parentheses, but we can't wrap a block in parentheses. And so, when we wrap the {} characters in parens, the engine can figure out that we're trying to create an object, not a block.

03 Object Destructuring#

Object destructuring offers a sleek way to extract some variables from an object.

Here's a quick example:

object-destructuring-example
const user = {
  name: 'François Bouchard',
  city: 'Saint-Louis-du-Ha! Ha!',
  province: 'Québec',
  country: 'Canada',
  postalCode: 'A1B 2C3',
};

const { name, country } = user;

console.log(name); // 'François Bouchard'
console.log(country); // 'Canada'

This is effectively the same thing as doing this:

traditional-property-access
const name = user.name;
const country = user.country;

We can pluck out as many or as few values as we want.

Renaming extracted values#

Consider this situation:

naming-conflict
const name = 'Hello!';

const user = { name: 'Marie-Hélene Pelletier' };

const { name } = user;
// Uncaught SyntaxError:
// Identifier 'name' has already been declared

We tried to destructure the name property into its own variable, but we run into an issue: there's already a variable called name!

In these cases, we can rename the value as we unpack it:

renaming-destructured-values
const name = 'Hello!';

const user = { name: 'Marie-Hélene Pelletier' };

const { name: userName } = user;

console.log(name); // 'Hello!'
console.log(userName); // 'Marie-Hélene Pelletier'

Default values#

Here's a question: what happens if we try to destructure a key from an object which isn't defined?

undefined-property
const user = { name: 'Marie-Hélene Pelletier' };

const { status } = user;

console.log(status); // ???

Well, user.status isn't defined, and so status will be set to undefined.

If we want, we can set a default value using the assignment operator:

default-values
const { status = 'idle' } = user;

if the user object has a status property, that value will be plucked out and assigned to the new status constant. Otherwise, status will be assigned to the string "idle".

In order words, it's a modern version of this:

traditional-default-value
const status = typeof user.status === 'undefined'
  ? 'idle'
  : user.status;

Destructuring function parameters#

Let's suppose we have a function which takes an object as its first parameter:

function-with-object-parameter
function validateUser(user) {
  if (typeof user.name !== 'string') {
    return false;
  }

  if (user.password.length < 12) {
    return false;
  }

  return true;
}

If we wanted, we could destructure these values at the top of the function:

destructuring-inside-function
function validateUser(user) {
  const { name, password } = user;

  if (typeof name !== 'string') {
    return false;
  }

  if (password.length < 12) {
    return false;
  }

  return true;
}

Using parameter destructuring, we can do this destructuring right in the function parameters:

parameter-destructuring
function validateUser({ name, password }) {
  if (typeof name !== 'string') {
    return false;
  }

  if (password.length < 12) {
    return false;
  }

  return true;
}

All 3 of these code snippets are equivalent, but many developers enjoy using function parameter destructuring. It's especially popular in React to destructure the props in our components.

Named arguments#

Some languages have "named arguments", where the developer can choose to label the arguments they're passing into a function.

JavaScript doesn't have this feature, and as a result, it can be difficult to understand what role different arguments play.

For example, suppose you're working on a codebase, and you stumble upon this code:

mysterious-arguments
trackSession(user.id, 5, null);

Why does this function receive a 5 and a null? It's totally mysterious. We'd need to hunt down the file where this function is defined to understand what these values are.

Suppose, though, that trackSession took an object as its only argument.

Then it would look like this:

named-arguments-pattern
trackSession({
  userId: user.id,
  version: 5,
  additionalMetadata: null,
});

By supplying an object, we effectively have to label each value! This works quite a lot like "named arguments" in other languages!

When I write a function, I'll often use parameter destructuring specifically so that I can pass an object in, rather than a bunch of individual arguments. That way, it's easier to understand what the provided data is.

Default parameter values#

Like with typical object destructuring, we can supply default values to be used for parameters.

Here's a quick example:

default-parameter-values
function sendApiRequest({ method = 'GET', numOfRetries }) {
  // Stuff
}

When I call this function, I can supply my own value for method:

custom-method-value
sendApiRequest({ method: 'PUT', numOfRetries: 4 });

…Or, if I want it to be equal to GET, I can omit it entirely:

default-method-value
sendApiRequest({ numOfRetries: 4 });
Gotcha: argument must be provided

Let's suppose we add default values for both of the properties in our function:

function sendApiRequest({ method = 'GET', numOfRetries = 5 }) {
  // Stuff
}

We then call this function without specifying an argument, since we want to use all of the default values:

sendApiRequest();

Unfortunately, we get an error:

Uncaught TypeError: Cannot read properties of undefined (reading 'method')

Here's the problem: we're destructuring the method and numOfRetries properties from an object, but when we call the sendApiRequest function, we're not providing an object.

Here's another example of the same issue, which might make it clearer:

function sendApiRequest(properties) {
  // `properties` is undefined, and so we'll get the same
  // error when we try and extract values from it:
  const {
    method = 'GET',
    numOfRetries = 5
  } = properties;
}

sendApiRequest();

To solve this problem, we can set a default value for the first parameter, the object we're destructuring from:

function sendApiRequest(
  { method = 'GET', numOfRetries = 5 } = {}
) {
  // Stuff
}

sendApiRequest(); // ✅ No problem!

When we call sendApiRequest without passing in any arguments, the first parameter gets initialized to an empty object. Then, the method and numOfRetries properties are initialized using their default values, since they weren't defined in that empty object.

This stuff gets pretty complex. You certainly don't need to take advantage of all of this modern syntax. I just wanted to let you know that this is an issue you might run into.

04 Property Value Shorthand#

Objects in modern JavaScript have a nifty little trick up their sleeve. It's a small thing, but if you're not aware of it, it can cause a lot of confusion.

Suppose we have the following code:

traditional-object-creation
const name = 'Ahmed';
const age = 26;

const user = {
  name: name,
  age: age,
};

console.log(user);
// { name: 'Ahmed', age: 26 }

We're creating a user object with two properties, name and age. We're assigning those properties to variables of the same name.

It does feel a bit redundant, though, doesn't it? We're assigning name to name, and assigning age to age.

Modern JavaScript gives us a convenient shorthand we can use in this situation:

property-value-shorthand
const name = 'Ahmed';
const age = 26;

const user = {
  name,
  age,
};

console.log(user);
// { name: 'Ahmed', age: 26 }

This is known as the property value shorthand. When creating an object, we can omit the values if they share the same name as the property.

The end result is the same, but it's a bit more terse.

05 Array Destructuring#

Suppose we have some data that lives in an array, and we want to pluck it out and assign it to a variable.

Traditionally, this has been done by accessing an item by index, and assigning it a value with a typical assignment statement:

traditional-array-access
const fruits = ['apple', 'banana', 'cantaloupe'];
const firstFruit = fruits[0];
const secondFruit = fruits[1];

Destructuring assignment offers us a nicer way to accomplish this task:

array-destructuring
const fruits = ['apple', 'banana', 'cantaloupe'];
const [firstFruit, secondFruit] = fruits;

The first time you see it, those array brackets before the = look pretty wild.

Here's how I think about it: when the [ ] characters are used after the assignment operator (=), they're used to package items into an array. When they're used before, they do the opposite, and they unpack items from an array:

packing-vs-unpacking
// Packing 3 values into an array:
const array = [1, 2, 3];

// Unpacking 3 values from an array:
const [first, second, third] = array;

Ultimately, this isn't really a game-changer, but it's a neat little party trick that can tidy things up a bit.

Disclaimer

Skipping items#

What if we only wanted to grab the second item, and not the first? Can we use destructuring assignment in that case?

In fact, we can:

const fruits = ['apple', 'banana', 'cantaloupe'];
const [, secondFruit] = fruits;

We've removed firstFruit, but kept the comma. Essentially, we've left a gap where the first variable used to be. If you're familiar with sheet music, you can think of it like a rest.

Technically, we can leave many gaps, picking and choosing the items we want:

const fruits = [
  'apple',
  'banana',
  'cantaloupe',
  'durian',
  'eggplant'
];

const [, secondFruit,,, fifthFruit] = fruits;

console.log(secondFruit); // 'banana'
console.log(fifthFruit); // 'eggplant'

This is an interesting little trick with destructuring assignment, but honestly I don't know if it's a great idea in complex cases like this. It feels simpler to me to use the standard notation:

const secondFruit = fruits[1];
const fifthFruit = fruits[4];

06 JavaScript Modules#

For a long time, JavaScript had no built-in module system.

In the early days, we wrote our JavaScript programs in .js files, and loaded them all up via <script> tags in our HTML files. This worked alright, but it meant that every script shared the same environment; variables declared in one file could be accessed in another. It was a bit of a mess.

Several third-party solutions were invented, around 2009-2010. The most popular were RequireJS and CommonJS.

Starting in the mid-2010s, however, JS got its own native module system! And it's pretty cool.

Basic premise#

When we work with a JS module system, every file becomes a "module". A module is a JavaScript file that can contain one or more exports. We can pull the code from one module into another using the import statement.

If we don't export a piece of data from one module, it won't be available in any other modules. The only way to share anything between modules is through import/export.

In addition to the code we write in our codebase, we can also import third-party modules like React.

The benefit of this rather complex system is that everything is isolated by default. We have to go out of our way to share data between modules. Imports/exports are the "bridges" between modules, and by strategically placing them, we can make complex programs easier to reason about.

Named exports#

Each file can define one or more named exports:

data.js
export const significantNum = 5;

export function doubleNum(num) {
  return num * 2;
}

In this case, our data.js module uses the export keyword to make a piece of data, significantNum, available to other files.

We're also exporting a function, doubleNum. We can export any JavaScript data type, including functions and classes.

In our main file, index.js, we're importing both of these exports:

index.js
import { significantNum, doubleNum } from './data';

Inside the curly braces, we list each of the imports we want to bring in, by name. We don't have to import all of the exports, we can pick just the ones we need.

The string at the end, './data', is the path to the module. We're allowed to omit the .js suffix, since it's implied.

The module system uses a linux-style relative path system to locate modules. A single dot, ., refers to the same directory. Two dots, .., refers to a parent directory.

Export statements#

It's conventional to export variables as they're declared, like this:

export-declaration
export const significantNum = 5;

It's also possible to export previously-declared variables using squiggly brackets, like this:

export-statement
const significantNum = 5;

export { significantNum };

Curiously, this syntax is quite rare — I only recently learned it was possible to do this! When it comes to named exports, it's much more common to export them right when they're declared.

We can also export functions as they're declared, like this:

export-function
// Produces a named export called `someFunction`:
export function someFunction() { /* ... */ }

Renaming imports#

Sometimes, we'll run into naming collisions with named imports:

naming-collision
import { Wrapper } from './Header';
import { Wrapper } from './Footer';
// 🚫 Identifier 'Wrapper' has already been declared.

This happens because named exports don't have to be globally unique. It's perfectly valid for both Header and Footer to use the same name:

header-footer-example
// Header.js
export function Wrapper() {
  return <header>Hello</header>;
}

// Footer.js
export function Wrapper() {
  return <footer>World</footer>;
}

We can rename imports with the as keyword:

renaming-imports
import { Wrapper as HeaderWrapper } from './Header';
import { Wrapper as FooterWrapper } from './Footer';
// ✅ No problems

Within the scope of this module, HeaderWrapper will refer to the Wrapper function exported from the /Header.js module. Similarly, FooterWrapper will refer to the other Wrapper function exported from /Footer.js.

Default exports#

There's a separate type of export in JS modules: the default export.

Let's look at an example:

data.js
const magicNumber = 100;

export default magicNumber;

When it comes to default exports, we always export an expression:

default-export-rules
// ✅ Correct:
const hi = 5;
export default hi;

// 🚫 Incorrect
export default const hi = 10;

Every JS module is limited to a single default export. For example, this is invalid:

multiple-default-exports
const hi = 5;
const bye = 10;

export default hi;
export default bye;
// 🚫 SyntaxError: Too many default exports!

When importing default exports, we don't use squiggly brackets:

importing-default
// ✅ Correct:
import magicNumber from './data';

// 🚫 Incorrect:
import { magicNumber } from './data';

When we're importing a default export, we can name it whatever we want; it doesn't have to match:

naming-default-imports
// ✅ This works
import magicNumber from './data';

// ✅ This also works!
import helloWorld from './data';

A lot of these differences might seem arbitrary or confusing, but they all stem from this fact: Every module can have multiple named exports, but only a single default export.

For example, we need to use the correct name when importing a named export because we need to specify which export we want! But because there's only a single default export, we can name it whatever we want, there's no ambiguity.

When to use which#

Now that we've covered the fundamentals about named and default exports, you might be wondering: when should I use each type?

In fact, this is a question with no objectively "correct" answer. It all comes down to picking a convention that works for you.

For example, let's say that we have a file that holds a bunch of theme constants (colors and sizes and fonts and stuff). We could structure it like this:

theme-default-export
const THEME = {
  colors: {
    red: 'hsl(333deg 100% 45%)',
    blue: 'hsl(220deg 80% 40%)',
  },
  spaces: [
    '0.25rem',
    '0.5rem',
    '1rem',
    '1.5rem',
    '2rem',
    '4rem',
    '8rem',
  ],
  fonts: {
    default: '"Helvetica Neue", sans-serif',
    mono: '"Fira Code", monospace',
  },
}

export default THEME;

Or, we could use named exports:

theme-named-exports
export const COLORS = {
  red: 'hsl(333deg 100% 45%)',
  blue: 'hsl(220deg 80% 40%)',
};

export const SPACES = [
  '0.25rem',
  '0.5rem',
  '1rem',
  '1.5rem',
  '2rem',
  '4rem',
  '8rem',
];

export const FONTS = {
  default: '"Helvetica Neue", sans-serif',
  mono: '"Fira Code", monospace',
}

We can use either a single "grouped" default export, or lots of individual named exports. Both of these options are perfectly valid.

Here's a convention I like to follow, though: if a file has one obvious "main" thing, I make it the default export. Secondary things, like helpers and metadata, can be exported using named exports.

For example, a React component might be set up like this:

react-component-example
// components/Header.js
export const HEADER_HEIGHT = '5rem';

function Header() {
  return (
    <header style={{ height: HEADER_HEIGHT }}>
      {/* Stuff here */}
    </header>
  )
}

export default Header;

The main "thing" in this Header.js file is the Header component, and so it uses the default export. Anything else will use named exports.

07 Array Iteration Methods#

One of the novel things about React is that JSX doesn't include any iteration helpers.

In most templating languages, you have custom syntax like {{#each}} to help you iterate through a set of data. In frameworks like Angular and Vue, you have directives like v-for to manage iteration.

React doesn't provide any abstractions here, and so we'll rely on the built-in methods that are part of the JavaScript language, methods like map and filter.

If we can become comfortable with these core methods, we'll have a much easier time doing iteration in React. In this section, we'll learn about some of the most common and useful methods.

forEach#

We use forEach when we want to perform some sort of action on every item in an array.

Here's an example:

forEach-example
const pizzaToppings = [
  'cheese',
  'avocado',
  'halibut',
  'custard',
];

pizzaToppings.forEach((topping) => {
  console.log(topping);
});

In this example, we're logging the value of each topping, one at a time. Here's what the console output would be:

cheese
avocado
halibut
custard

The forEach method accepts a function as its argument. This is commonly known as a callback function.

The term "callback function" refers to a function that we pass to another function. In this case, we write an arrow function that looks like this:

callback-function
(topping) => {
  console.log(topping);
}

We don't call this function ourselves; instead, we pass it as an argument to the forEach method. The JavaScript engine will call this function for us, supplying the topping argument as it iterates over the array.

Accessing the index#

The callback we pass to forEach takes a second optional parameter:

forEach-with-index
const pizzaToppings = [
  'cheese',
  'avocado',
  'halibut',
  'custard',
];

pizzaToppings.forEach((topping, index) => {
  console.log(index, topping);
});

The index is the position in the array of the current item, starting from 0. This code would log:

0 cheese
1 avocado
2 halibut
3 custard

A common format#

JavaScript gives us several tools for iterating over the items in an array. We could have used a for loop, and it's arguably much simpler. There are no complicated callback functions! So why should we learn about .forEach?

Here's the biggest advantage: forEach is part of a family of array iteration methods. Taken as a whole, this family allows us to do all sorts of amazing things, like finding a particular item in the array, filtering a list, and much more.

All of the methods in this family follow the same basic structure. For example, they all support the optional index parameter we just looked at!

Let's look at another member of this family: the filter method.

filter#

Things start to get really interesting with filter.

Here's a quick example:

filter-example
const students = [
  { name: 'Aisha', grade: 89 },
  { name: 'Bruno', grade: 55 },
  { name: 'Carlos', grade: 68 },
  { name: 'Dacian', grade: 71 },
  { name: 'Esther', grade: 40 },
];

const studentsWhoPassed = students.filter(student => {
  return student.grade >= 60;
});

console.log(studentsWhoPassed);
/*
  [
    { name: 'Aisha', grade: 89 },
    { name: 'Carlos', grade: 68 },
    { name: 'Dacian', grade: 71 },
  ]
*/

In many ways, filter is very similar to forEach. It takes a callback function, and that callback function will be called once per item in the array.

Unlike forEach, however, filter produces a value. Specifically, it produces a new array which contains a subset of items from the original array.

Typically, our callback function should return a boolean value, either true or false. The filter method calls this function once for every item in the array. If the callback returns true, this item is included in the new array. Otherwise, it's excluded.

Important to note: The filter method doesn't modify the original array. This is true for all of the array methods discussed in this lesson.

Here's one more example:

filter-even-numbers
const nums = [5, 12, 15, 31, 40];

const evenNums = nums.filter(num => {
  return num % 2 === 0;
});

console.log(nums); // Hasn't changed: [5, 12, 15, 31, 40]
console.log(evenNums); // [12, 40]

map#

Finally, we have the map method. This is the most commonly-used array method, when working with React.

Let's start with an example:

map-example
const people = [
  { name: 'Aisha', grade: 89 },
  { name: 'Bruno', grade: 55 },
  { name: 'Carlos', grade: 68 },
  { name: 'Dacian', grade: 71 },
  { name: 'Esther', grade: 40 },
];

const screamedNames = people.map(person => {
  return person.name.toUpperCase();
});

console.log(screamedNames);
/*
  ['AISHA', 'BRUNO', 'CARLOS', 'DACIAN', 'ESTHER']
*/

In many ways, map is quite a lot like forEach. We give it a callback function, and it iterates over the array, calling the function once for each item in the array.

Here's the big difference, though: map produces a brand new array, full of transformed values.

The forEach function will always return undefined:

forEach-returns-undefined
const nums = [1, 2, 3];

const result = nums.forEach(num => num + 1);

console.log(result); // undefined

By contrast, map will "collect" all the values we return from our callback, and put them into a new array:

map-returns-array
const nums = [1, 2, 3];

const result = nums.map(num => num + 1);

console.log(result); // [2, 3, 4]

Like filter, map doesn't mutate the original array; it produces a brand-new array.

Also, the new array will always have the exact same length as the original array. We can't "skip" certain items by returning false or not returning anything at all:

map-same-length
const people = [
  { id: 'a', name: 'Aisha' },
  { id: 'b' },
  { id: 'c' },
  { id: 'd', name: 'Dacian' },
  { id: 'e' },
];

const screamedNames = people.map(person => {
  if (person.name) {
    return person.name.toUpperCase();
  }
});

console.log(screamedNames);
/*
  ['AISHA', undefined, undefined, 'DACIAN', undefined]
*/

Here's a helpful way to think about map: it's exactly like forEach, except it "saves" whatever we return from the callback into a new array.

Accessing the index with map#

Like the other methods we've seen, we can pass a second parameter to access the current item's index:

map-with-index
const people = [
  { name: 'Aisha', grade: 89 },
  { name: 'Bruno', grade: 55 },
  { name: 'Carlos', grade: 68 },
  { name: 'Dacian', grade: 71 },
  { name: 'Esther', grade: 40 },
];

const numberedNames = people.map((person, index) => {
  return `${index}-${person.name}`;
});

console.log(numberedNames);
/*
  ['0-Aisha', '1-Bruno', '2-Carlos', '3-Dacian', '4-Esther']
*/

Additional methods#

We've seen 3 of the most common methods here, but there are actually several more that follow this same format, including find (selecting a single item from the array), every (checking if all items in an array fulfill some condition), and reduce (combine all items in the array into a single final value).

You can learn more about these methods on MDN.

08 Truthy and Falsy#

Let's consider the following JavaScript statement:

truthy-example
const user = {
  name: 'J. Script',
};

if (user.name) {
  console.log('This user has a name!');
}

We have a user object, and we want to run a console.log depending on a condition. The condition is the JavaScript expression user.name.

Interestingly, user.name isn't a boolean value. The user's name is neither true nor false. In many programming languages, this would be an illegal operation. How should the language know if some random string is sufficient or not??

In JavaScript, every value is either "truthy" or "falsy". A truthy value is one that counts as true when it comes to conditions.

Most values in JavaScript are truthy. It's faster to list all of the falsy values:

  • false
  • null
  • undefined
  • '' (empty string)
  • 0 (and related values, like 0.0 and -0)
  • NaN

In the code above, user.name is a truthy value, because it's a string with at least 1 character. Every string other than '' is truthy.

Somewhat surprisingly, [] (empty array) and {} (empty object) are truthy values, not falsy. This means that every object/array is truthy.

Converting to boolean#

Sometimes, it's beneficial to convert a truthy value into true, or a falsy value into false.

The most declarative way to do this is to use the Boolean() constructor:

boolean-constructor
Boolean(4); // true
Boolean(0); // false
Boolean([]); // true
Boolean(''); // false

There's another more-common way to convert to boolean. And, if you're not familiar with it, it can look a bit funky:

double-not-operator
!!4; // true
!!0; // false
!![]; // true
!!''; // false

!! isn't actually a JavaScript operator; we're repeating the NOT operator (!) twice.

We can use ! to flip a boolean value:

not-operator
!true; // false
!false; // true

If we use the ! with a non-boolean value, it will flip a truthy value to false, or a falsy value to true:

not-with-non-boolean
!4; // false, since 4 is truthy
!0; // true, since 0 is falsy

We can stack multiple ! operators to flip it back and forth:

multiple-not-operators
!4; // false
!!4; // true
!!!4; // false
!!!!4; // true

To break down what's going on here: Each ! is evaluated, one at a time. It's as if there were parens around each set, like this:

not-operator-breakdown
!(!(!4))
    ^^ !4 resolves to `false`

!(!false)
  ^^^^^^ !false resolves to `true`

!true
^^^^^ !true resolves to `false`

I'm not sure why !! has become the de facto standard way of converting to boolean; the Boolean() constructor seems a lot more intuitive to me!

09 Logical Operators#

If you've been working with JavaScript for a while, you're probably already familiar with the AND operator (&&) and the OR operator (||):

basic-logical-operators
const isLoggedIn = true;
const userRole = 'administrator';

if (isLoggedIn && userRole === 'administrator') {
  // This code only runs if BOTH conditions above are truthy
}

There's something that many JavaScript developers don't realize about these operators, however: they also act as control flow operators.

Let's test your knowledge with a quick multiple-choice question: What is the value of result?

logical-operator-quiz
const myAge = 35;
const result = myAge < 50 && myAge;
  1. true
  2. false
  3. 35
  4. None of the above

It is totally reasonable to expect that the answer would be true:

  • myAge < 50 resolves to true
  • myAge resolves to 35, which is a truthy value
  • If both sides are truthy, the whole thing should evaluate to true, right?

Afraid not! The && operator isn't designed to produce a boolean value. It's designed to resolve to one of the expressions on either side of the operator.

You can think of && like a gate. We're allowed to move through the gate if the left-hand side is truthy.

This becomes clearer when we chain multiple &&s together:

chained-and-operators
const numOfChildren = 4;
const parkingHasBeenValidated = true;
const signatureOnWaiver = '';

const admissionTicket =
  numOfChildren &&
  parkingHasBeenValidated &&
  signatureOnWaiver &&
  generateTicket();

The first expression in this chain is numOfChildren, which is 4. This is a truthy value, and so the first && gate is unlocked.

The second expression is parkingHasBeenValidated, which is true. We move through the second gate.

The third expression, signatureOnWaiver, is an empty string (''), which is falsy. This means that the third && gate stays locked. We can't go any further. The current expression, '', is what gets resolved.

As a result, admissionTicket would be assigned ''. Not false.

Let's suppose signatureOnWaiver wasn't an empty string:

all-truthy-and-chain
const numOfChildren = 4;
const parkingHasBeenValidated = true;
const signatureOnWaiver = 'Becky Chambers';

const admissionTicket =
  numOfChildren &&
  parkingHasBeenValidated &&
  signatureOnWaiver &&
  generateTicket();

Now, when we get to the third expression, we get a truthy value (the string "Becky Chambers"). The final gate is unlocked.

Because all of the gates are unlocked, the final expression will be passed along. admissionTicket will be equal to whatever the generateTicket function returns.

Here's how we'd set up exactly the same code, but using if/else instead of logical operators:

if-else-equivalent
let admissionTicket;

if (!numOfChildren) {
  admissionTicket = numOfChildren;
} else if (!parkingHasBeenValidated) {
  admissionTicket = parkingHasBeenValidated;
} else if (!signatureOnWaiver) {
  admissionTicket = signatureOnWaiver;
} else {
  admissionTicket = generateTicket();
}

admissionTicket will be assigned to the first falsy expression. If we make it through all of the gates, admissionTicket will be assigned to the final expression, whether it's truthy or falsy.

There's one more interesting takeaway here: The generateTicket function is only called if we make it through all the gates. In the first example, when signatureOnWaiver was falsy, the generateTicket function was never called.

This is because of short-circuiting. If one of the expressions before an && operator evaluates to a falsy value, the rest of the expressions are skipped. They won't be executed at all.

Here's a less-complex example:

short-circuiting-example
false && console.log('I will never run');

Because false is a falsy value, the && gate remains locked, and the console.log never fires. It's the same as if the console.log was within an if statement:

if-statement-equivalent
if (false) {
  console.log('I will never run');
}

The OR operator#

The || operator is exactly like the && operator, with one key difference: || doesn't stop and provide the first falsy value. It stops and provides the first truthy value.

For example:

or-operator-example
const userImageSrc = null;
const teamImageSrc = null;
const defaultImageSrc = '/images/cat.jpg';

const src = userImageSrc || teamImageSrc || defaultImageSrc;

We check the first expression, userImageSrc, which is null. Because it's falsy, the || gate is unlocked, and we proceed onwards.

The second expression, teamImageSrc, is also null, and so the second || gate unlocks.

As with &&, when we make it to the final item, the final item is provided regardless of whether it's truthy or falsy. And so src will be assigned to defaultImageSrc.

However, in this case:

or-operator-truthy-example
const userImageSrc = '/images/my-avatar.png';
const teamImageSrc = null;
const defaultImageSrc = '/images/cat.jpg';

const src = userImageSrc || teamImageSrc || defaultImageSrc;

The first item, userImageSrc, is truthy! Because it's truthy, the first || gate remains locked, and src is assigned to userImageSrc.

Here's how we'd represent this code using if/else:

or-operator-if-else-equivalent
let src;

if (userImageSrc) {
  src = userImageSrc;
} else if (teamImageSrc) {
  src = teamImageSrc;
} else {
  src = defaultImageSrc;
}

10 Assignment Vs. Mutation#

in progress...

11 Rest / Spread#

in progress...

12 Intervals and Timeouts#

in progress...

13 Global Events#

in progress...

14 Async / Await#

in progress...

15 HTTP Methods#

in progress...

Fetch#

in progress...

16 HTTP Status Codes#

in progress...