JavaScript

JavaScript carries a lot of historical burdens due to browsers maintaining backward compatibility, which prevents the removal of old features. Meanwhile, new features continue to be added, as it is the most widely used programming language on earth. As a result, the language can feel chaotic.

Don't be afraid. JavaScript is a reasonable language if you only use the good parts.

Where to Run JavaScript

One option is to use a Browser Console. In Chrome, you can open the JavaScript console by pressing Cmd + Opt + J on Mac or Ctrl + Shift + J on Windows.

Another option is to use Node.js REPL. Install Node.js using folowing command:

brew install node

Then start the REPL:

$ node
Welcome to Node.js v20.6.1.
Type ".help" for more information.
>

To exit, press Ctrl + D or Ctrl + C .

Basic Syntax

Variable

const variables can't be reassigned, which makes code easier to read. let allows for reassignment. There is also var, don't use it.

let name = "Alice"
const age = 7

Array

const fruits = ["apple", "banana", "cherry"]

fruits
  .filter(x => x.length < 6)
  .map(x => x.toUpperCase()) // ["APPLE"]

fruits.slice(0, 2) // ["apple", "banana"]

Object

const obj = {name: "Alice", age: 7}
Object.fromEntries([["name", "Alice"], ["age", 7]])
Object.entries(obj)

Function

function greet(name) {
  return `Hello, ${name}!`
}

Calling a function

greet("JS")
greet.call(null, "JS")
greet.apply(null, ["JS"])

The first argument to call and apply is the this context:

function greet() {
  return `Hello, ${this.name}!`
}

greet.call({name: "JS"})

this can also be specified this way:

function greet() {
  return `Hello, ${this.name}!`
}

const alice = {name: "Alice", greet}
alice.greet()

Arrow Function

The most significant difference between arrow functions and traditional function is their lexical this binding. Unlike traditional functions, arrow functions do not create their own this context. Instead, they inherit the this value from the enclosing scope. This is particularly useful in scenarios like callbacks.

const greet = (name) => {
  return `Hello, ${name}!`
}

Arrow functions with a single expression implicitly returns the value of that expression:

const greet = name => `Hello, ${name}!`

Class

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  greet() {
    console.log(`Hello, my name is ${this.name}`)
  }
}

const person = new Person("Alice", 7)
person.greet()

Destructuring

const person = { name: "Alice", age: 30 }
const { name, age } = person
const [first, ...rest] = [1, 2, 3]

Spread

const arr1 = [1, 2, 3]
const arr = [...arr1, 4, 5]
const dict1 = { key: "val" }
const dict = { ...dict1, key2: "val2" }

Asynchronous Programming

Asynchronous programming in JavaScript is typically done using callbacks, event pub/sub, coroutines, streams, and Promises with async/await.

In modern JavaScript, Promises and async/await are preferred for their improved readability and maintainability.

Creating a Promise:

function delay(milliseconds) {
  return new Promise((resolve, reject) => setTimeout(resolve, milliseconds))
}

Comsume a promise:

delay(1000).then(result => {
  console.log('one second passed')
}).catch(error => {
  console.error(error)
})

Or use async/await:

async function myFunction() {
  await delay(1000)
  await op1()
  await delay(1000)
  await op2()
}

This is way better than Callbacks:

function myFunction(callback) {
  setTimeout(() => {
    op1().then(() => {
      setTimeout(() => {
        op2().then(() => callback()).catch(callback)
      }, 1000)
    }).catch(callback)
  }, 1000)
}