JavaScript

JavaScript is by some measures the most popular programming language in the world. It is definitely in the top 5 need-to-know languages for aspiring programmers.

Overview

JavaScript, the language:

The first implementation was written by Brendan Eich in 10 days in May of 1995. The language was originally named Mocha, then was renamed LiveScript in September, 1995, and then renamed JavaScript sometime after that.

Although over 25 years old, JavaScript is still an evolving language. A lot of the code in the wild, and a lot of the code you see online and in books, is quite old and may not follow modern best practices. This is just the way the world is.

Here are some very useful resources:

Getting Started

You can run JavaScript programs (scripts) from the command line or directly within a web browser.

Web Applications

Find your browser’s “Developer Tools” and go to the “Console” tab. At the prompt, enter and run the one-line program:

console.log("Hello, world.")

This script consists of one statement which is a call to the function that is the value of the property named log of the property named console of the global object.

The browser’s console is not the main show: on the web, the console is generally used only for informative messages and error messages. You normally want to write to the browser’s document. The document’s body can show either plain text (through its textContent property) or beautiful structured content (through its innerHTML property). Run this code from the console:

document.body.textContent = "Hello, world."

Ohai! You made it back! That program blew away this page that you were looking at, didn’t it? Don’t do that again.

The console is wonderful for trying things out yourself, but you’re probably here to learn how to write programs for people to use. If you want to build programs that anyone can run on the web, there are many, many options, including CodeSandbox, Glitch, The p5.js Web Editor, CodePen, and Replit to name a few. These tools will manage your program files and host your applications for you. Here’s an example using CodeSandbox:

CLASSWORK
Open the sandbox and fork it to create a project of your own using this project as your starter code. Make some changes to it. Notice that CodeSandbox gives you a URL where your project is hosted. Share this link with others in the class so you can run each other’s applications.

You don’t have to use an online development environment to develop JavaScript web applications. You can create files locally, on your own machine. You can upload them to your favorite hosting provider, or simply share your source code with friends and let them build and run the app by themselves.

Command Line Applications

JavaScript is not relegated to web apps that run in the browser; it competes just fine with Python for writing command line scripts. Here’s a command line JavaScript program. Store this in a file and run it in your terminal. You will need to install Node on your own first; the browser may come with JavaScript engine built-in but your command line probably does not.

triple.js
for (let c = 1; c < 50; c++) {
  for (let b = 1; b < c; b++) {
    for (let a = 1; a < b; a++) {
      if (a ** 2 + b ** 2 === c ** 2) {
        console.log(`(${a}, ${b}, ${c})`)
      }
    }
  }
}
$ node triple.js
(3, 4, 5)
(6, 8, 10)
(5, 12, 13)
(9, 12, 15)
(8, 15, 17)
(12, 16, 20)
(15, 20, 25)
(7, 24, 25)
(10, 24, 26)
(20, 21, 29)
(18, 24, 30)
(16, 30, 34)
(21, 28, 35)
(12, 35, 37)
(15, 36, 39)
(24, 32, 40)
(9, 40, 41)
(27, 36, 45)

Here’s one that generates a frequency table of words from a file, writing the word and its number of occurrences, ordered by frequency. It’s packed with interesting linguistic features. Don’t try to understand everything right now!

wordcount.mjs
import readline from 'readline/promises'
const reader = readline.createInterface(process.stdin)
const counts = new Map()

for await (const line of reader) {
  for (let [word] of line.toLowerCase().matchAll(/[\p{L}’']+/gu)) {
    counts.set(word, (counts.get(word) ?? 0) + 1)
  }
}
for (let [word, count] of [...counts.entries()].sort((a, b) => b[1] - a[1])) {
  console.log(`${word} ${count}`)
}

Now that you’ve seen what JavaScript looks like, we are ready to learn JavaScript from the beginning.

Data Types

All values belong to one of 8 types:

TypeValues of the Type
UndefinedOnly one value: undefined. Means “I don’t know,” “I don’t care”, or “None of your business.”
NullOnly one value: null. Means “no value.”
BooleanOnly two values: true and false.
NumberThe IEEE 754 64-bit floating point values. Values that are integers can be expressed in binary, octal, decimal, or hex; non-integers must be expressed in decimal. Examples:
  • 8
  • 7.23342
  • 6.02e23
  • 0xff3e
  • 0b11010100001010
  • 0o237
  • Infinity
  • NaN
BigIntArbitrary-precision integers. Needed because the Number type can not represent most integers with a magnitude above 9007199254740992. Examples:
  • 3n
  • 2098321521257182187525313919187155317815353517831735173173551735173n
StringImmutable sequences of zero or more UTF-16 code units. You can delimit them with apostrophes, quotation marks, or backticks. Examples:
  • "hello"
  • "She said 'I don’t think so 😎'... (╯°□°)╯︵ ┻━┻)"
  • 'x = "4"'
  • "abc\tdef\"\r\nghi\n🏄‍♀️🏀\n"
  • "Olé"
  • "Ol\xe9"
  • 'Will I?\u043d\u0435\u0442\u263a'
  • `The sum of ${x} and ${y} is probably ${x + y}`
Only backtick-delimited literals can span lines and support interpolation.
SymbolUnique things. Every time you create a symbol, you get a new thing, different from all other symbols. This is not necessarily true of strings. Examples:
  • Symbol()
  • Symbol('dog')
  • Symbol('dog') // different from the one above
ObjectEverything that isn’t one of the above types. Examples:
  • {}
  • {latitude: 74.2, longitude: -153.11}
  • [true, true, {last: false, value: 'okay'}, [0, 0, 2]]
  • new Set([5, 1, 2])
  • new Date(2000, 12, 31)
  • (x, y) => x * x + y * y
  • /Boo+m!?/gi
Certain kinds of objects, such as arrays, functions and regular expressions have special syntactic forms, but to JavaScript they are just considered to have the type Object.

Built-in Operations by Type

Some common operations on numbers (many but not all of these work on bigints):

Some common operations on strings:

These work on numbers, bigints, and strings:

These work on any value:

x === y
produces whether x and y are EQUAL
x !== y
same as !(x === y)
x == y
produces whether x and y are SIMILAR
x != y
same as !(x == y)
x && y
if x is falsy then x, else y
x || y
if x is truthy then x, else y
!x
if x is truthy then false, else true
x ?? y
if x is nullish (null or undefined) then y, else x

Determining the Type of an Expression

In JavaScript, types are not objects in their own right; instead, if you ask for the type of an object, you will get a string. Unfortunately, the normal way you do this is fairly messed up:

typeof undefined      // 'undefined'
typeof null           // 'object'   -- UNFORTUNATE: This is considered a design flaw
typeof false          // 'boolean'
typeof 93.8888        // 'number'
typeof "Hi"           // 'string'
typeof 8n             // 'bigint'
typeof {x: 1, y: 2}   // 'object'
typeof [1, 2, 3]      // 'object'   -- sensible, arrays are just objects
typeof /o*h/          // 'object'   -- sensible, regexes are just objects
typeof (x => x + 1)   // 'function' -- UNFORTUNATE: functions are actually objects too
typeof NaN            // 'number'   -- sounds hilarious but makes sense

You can also use the constructor property, but there’s a lot behind the scenes to it: Booleans, numbers, bigints, strings, and symbols are internally converted into objects to make this work, (2) this won’t work for null and undefined, nor should it, and (3) the value produced might look like a type object, and you can certainly pretend it is, but it’s really a function object. Wild, but true. More on this later:

false.constructor            // Boolean
(3).constructor              // Number
95n.constructor              // BigInt
"hello".constructor          // String
Symbol('Dog').constructor    // Symbol
/a*b/.constructor            // RegExp
isFinite.constructor         // Function
[1,2,3].constructor          // Array
{x: 3, y: 5}.constructor     // Object
(new Date()).constructor     // Date
class Dog {};
d = new Dog();
d.constructor                // Dog
> undefined.constructor      // THROWS A TypeError
> null.constructor           // THROWS A TypeError

Speaking of types, we’ll see “Typed Arrays” later in these notes.

Weak Typing

Usually, if an operator doesn’t get operands of the right type, JavaScript silently performs conversions to get arguments of the right types. This is called weak typing. (In contrast, strong typing is when operands of the wrong type generate an error.)

When a boolean is expected:

When a number is expected:

When a string is expected:

A value that is false or would be converted to false is called falsy. All other values are truthy. (I did not make up those names.)

Exercise: For each of the following values, state whether they are truthy or falsy: 9.3, 0, [0], false, true, "", "${''}", `${''}`, 9.3, [], [[]], {}.
JavaScript isn’t 100% weakly-typed though

There are some occasions where a TypeError occurs, such as using a symbol where a number or string is expected, trying to access a property on null or undefined, and mixing numbers with bigints.

Bindings

You can bind names to expressions, and use the names later, for example:

const dozen = 3 * 4         // Binds the name dozen to the value 12
let favorite = 'Grimes'     // Binds the name favorite to the value "Grimes"
dozen = 2                   // Throws a TypeError because const bindings cannot be updated
console.log(dozen)          // 12
favorite = 'Austra'         // That's fine! let-bindings can be updated
console.log(favorite)       // Austra

Use const when the value bound to the name should not and must not change; use let otherwise. It is considered good programming practice to use const almost all the time. If you are updating a binding, ask yourself whether that is the best thing to do.

You can also bind with var, but don’t.

We’ll see why later.

By the way, the names you bind to values are called variables. If you bind them with const, you can call them constants if you wish.

Objects

A JavaScript object is a value made up by bundling together named properties, each of which has a value. A property is either a non-negative integer or a string or a symbol. A string property does not have to be given in quotes if it is a simple-enough word (that is, with no spaces or other non-word-like characters).

let part = {
  'serial number': '367DRT2219873X-785-11P',
  description: 'air intake manifold',
  'unit cost': 29.95
}

let person = {
  name: "Seán O'Brien",
  country: 'Ireland',
  birth: { year: 1981, month: 3, day: 17 },
  kidNames: {1: 'Ciara', 2: 'Bearach', 3: 'Máiréad', 4: 'Aisling' }
}

You can always access properties with square-bracket notation. If the property name is a simple string without spaces or punctuation or other oddities, you can use dot-notation.

person.country           // 'Ireland'
person['country']        // 'Ireland'
person.birth.year        // 1981
person.birth['year']     // 1981
person['birth'].year     // 1981
person['birth']['year']  // 1981
person.kidNames[4]       // 'Aisling'
person['kidNames'][4]    // 'Aisling'

const x = 'country'
person[x]                // 'Ireland'

Variables and properties directly store the values of non-objects, but they store references to objects (not the objects themselves). This is best understood pictorially:

sean.png

This is worth repeating

Variables and properties store references to objects, not the objects themselves.

The syntax for objects is pretty rich:

const seen = false
const metric = 'weight'
const location = { lat: 20, lon: 100}

const info = {
  color: 'blue',   // quotes for string property names not always needed
  "name": 'Ali',   // quotes are okay even if not needed
  seen,            // a shorthand property, same as seen:seen
  21: true,        // non-negative integer properties okay
  [metric]: 88,    // a computed property: the key is "weight"
  ...location,     // this spreads the properties of location into this object
}

Identity vs. Equality

The fact that objects are always stored indirectly via references has major ramifications for assignment and equality testing that you absolutely must understand:

const p = {x: 3, y: 5}   // The curly braces CREATE a new object
const q = {x: 3, y: 5}   // The curly braces CREATE a new object
const r = q              // Copy the reference (pointer) in box q into the box for r
p !== q                  // true, bc these two pointers point to different things
q === r                  // true, bc these two pointers point to the same thing

Try diagramming these variables and objects on your own. You should end up with this:

jsidentityequality.png

Note that p and q refer to different objects, but q and r refer to the same object. We say q and r are identical as the objects they refer to have the same identity. You can tell that p and q are not identical, but might they be “equal”? The objects they refer to have the same property values but alas, no, for JavaScript objects, equality and identity are the same.

Exercise: Do you know Python? If so, show that identity and equality in Python are not the same.

Shallow vs. Deep Copy

Just as the idea of object values as pointers impacts our understanding of equality, it also impacts how we understand assignment. Simple object assignment induces sharing, or aliasing, so the change made to an object through one variable can be seen through the other:

const home = {color: 'blue', location: {'lat': -30, 'lon': 11}}
const work = home
work.color = 'green'

Woops! You just colored your home green.

CLASSWORK
This section is getting technical, but it’s important, so we are going to do the details in class, together, with lots of pictures.

If we want work and home to vary independently, we need them to be distinct objects. We could have copied first, like so:

const home = {color: 'blue', location: {'lat': -30, 'lon': 11}}
const work = {...home}
work.color = 'green'

Okay, so we’ve broken the sharing of “top-level” properties color and location, but the location properties are still shared!

work.location.lat = -29
console.log(home.location.lat)    // -29

Writing work = {...home} is a shallow copy because it only copies the values of the properties of home, and if those values are themselves objects, only the pointer is copied! If you need to recursively make copies of those objects (and their “descendants”) you would be doing a deep copy. There’s no deep copy built-in to JavaScript, but you can find this functionality in third-party libraries, or try to write it yourself.

Exercise: Along with the notion of Shallow Copy and Deep Copy, there are the notions of Shallow Equal and Deep Equal. Venture a guess as to what they mean.

Destructuring

The left-hand side of assignment need not just be a single variable, it can be a pattern that is matched. Examples:

const point = { lat: 20, lon: -150 }
const { lat: y } = point              // Declares y to be 20
let { lat } = point                   // Declares lat to be 20 (shorthand property syntax)

const home = { color: 'green', location: { ...point, lon: 30 } }
let { color: c, location: { lat: a, lon: b } } = home
console.log(c, a, b)
        // prints "green 20 30"

let window = {title: 'App', w: 800, h: 600, padding: 2, margin: 10}
let { w, h, ...props } = window
console.log(w, h, props)
        // 800 600 { title: 'App', padding: 2, margin: 10 }
Why is it called Destructuring and not just Pattern Matching?

It’s both really. Consider this: if you had a variable called point bound to, an object with x and y properties, you could sprinkle point.x and point.y throughout your code. But if you wrote let { x, y } = point, you can just use x and y. You’ve destructured the point object into its components.

Prototypes

Like all useful programming languages, JavaScript has a mechanism to express collections of objects that are all related (structurally or behaviorally) to each other, i.e., to make something resembling a new data type.

Every object in JavaScript has a prototype. When looking up properties, the prototype chain is searched. Objects created with the { } syntax get a built-in, global prototype (details below), but if you use Object.create, you can set the new object’s prototype to whatever you want:

const protoCircle = {x: 0, y: 0, radius: 1, color: "black"}
const c1 = Object.create(protoCircle)
c1.x = 4
c1.color = "green" // Now c1.y === 0 and c1.radius === 1

jsobjwithproto.png

Here $c1$ has two own properties: x and color. It has several inherited properties, including y and radius from its own prototype, and a bunch of others from its prototype’s prototype.

Prototypes are very useful when creating a bunch of objects that look or behave similarly:

jsobjswithsharedproto.png

Exercise: Write the code that creates the scenario diagrammed in this figure.

Arrays

An array can be constructed with square brackets or Array(____) or new Array(____) or Array.of(____) or Array.from(____). You will use square brackets 99% of the time or more. It has a length property as well as non-negative integer properties.

const a = []
let b = [1, 5, 25]
let c = new Array()          // Same as [], but way lamer
c = new Array(4, 5, 6)       // Same as [4, 5, 6], but way lamer
c = new Array(10)            // 10 empty cells
const [d, e, f] = b          // d===1, e===5, f===25 (destructuring)
[c, b] = [b, false]          // c===[1, 5, 25], b===false

const g = [1, true, [1,2], {x:5, y:6}, "Hi"]
const h = {triple: {a:4, b:"dog", c:[1,null]}, 7: "stuff"}

g[1]           // true
g[2][0]        // 1
g.length       // 5
g[10] = 100    // g[5] through g[9] are empty cells
g.length       // 11
g.length = 2   // Now g is just [1, true]

Did you notice? Arrays can have empty cells. They’re...like...missing. They don’t contain null, nor undefined, nor NaN, they just don’t exist! But if you ask for its value, you will get undefined. Oh my!

Exercise: Experiment with arrays. Can we get to the third element of array dogs by saying dogs.2 or dogs["2"]? Or is dogs[2] the only way? What about dogs."2" or let x=2; dogs.x?
Exercise: Experiment some more with arrays. Does let x = ["dog"] define a one element array? What if we create a three-element array and try to print the value of its 12th element? What if we create a three-element array and set the value of its 12th element?
Exercise: What’s going on here?

let a = new Array(3)
a[0] = 1
a[1] = "hello"
a[2] = a

Here are some array operations:

// Construction
const a = [300, 900, 200, 400, 300, 700]
const b = [500, 300]
const c = Array.of(95, 33, 'dog', false)  // [95, 33, 'dog', false]
const d = Array.from('dog')               // ['d', 'o', 'g']
const e = [5, ...b, -10]                  // [5, 500, 300, -10] (note the spread operator)
const f = [b, ...b]                       // [[500, 300], 500, 300]

// Tests
Array.isArray(['x', true, 3])             // true
Array.isArray({0: 'x', 1: true, 2: 3})    // false

// Accessors (these do not change their operands)
a.includes(400)      // true
a.includes(7)        // false
a.indexOf(300)       // 0
a.lastIndexOf(300)   // 4
a.indexOf(2500)      // -1
a.slice(2, 5)        // [200, 400, 300]
a.slice()            // makes a shallow copy!
a.concat(b)          // [300, 900, 200, 400, 300, 700, 500, 300]
a.join('--')         // "300--900--200--400--300--700"
a.toString()         // '300,900,200,400,300,700'

// Mutators
a.push(3)            // returns 7, a is now [300, 900, 200, 400, 300, 700, 3]
a.pop()              // returns 3, a is now [300, 900, 200, 400, 300, 700]
a.unshift(8)         // returns 7, a is now [ 8, 300, 900, 200, 400, 300, 700 ]
a.shift()            // returns 8, a is now [300, 900, 200, 400, 300, 700]
a.reverse()          // a is now [ 700, 300, 400, 200, 900, 300 ]
a.sort()             // a is now [ 200, 300, 300, 400, 700, 900 ]
a.fill(9)            // a is now [ 9, 9, 9, 9, 9, 9 ]
e.splice(1, 2, 90, 70, 200, 6, 17)
                     // At position 1, remove 2 elements, and insert the others
                     // e is now [ 5, 90, 70, 200, 6, 17, -10 ]
e.copyWithin(1, 4, 6)
                     // shallow copy elements at positions [4, 6) to position 1
                     // e is now [ 5, 6, 17, 200, 6, 17, -10 ]

// Really Cool stuff
c.keys()             // An iterator that goes through 0, 1, 2, 3
[...c.keys()]        // [0, 1, 2, 3]
Array.from(c.keys()) // [0, 1, 2, 3] (though use the spread instead)
[...c.entries()]     // [[0, 95], [1, 33], [2, 'dog'], [3, false]]

for (let [i, x] of c.entries()) console.log(`${i} -> ${x}`)
                     // 0 -> 95
                     // 1 -> 33
                     // 2 -> dog
                     // 3 -> false
Exercise: Experiment with splice. Show that you can use it for inserting elements only, deleting elements only, or replacing elements only.
Exercise: Experiment with copyWithin. What if the target range overlaps the source range? What if (any of) the indexes are out of bounds? How are negative indexes handled?

You will sometimes see Array.from when programming in the web browser. Things like NodeLists and HTMLCollections look like arrays but they aren’t; use Array.from to turn them into real arrays if you need to do such things.

The array operations find, findIndex, map, filter, reduce, every, and some will be discussed later.

Functions

Functions come in two flavors: they are either arrow functions or non-arrow functions. Here are some arrow functions in action:

(x => x * 2 + 1)(20)                     // evaluates to 41
const square = x => x * x                
const now = () => new Date()
const average = (x, y) => (x + y) / 2
average(3, -4)                           // evaluates to -0.5

Non-arrow function values the function keyword, the Function constructor, or are created in a function declaration. When inside an object, you can use a shortcut syntax that also omits the function keyword:

function successor (x) {            // Non-arrow function
  return x + 1
}
const sum = function (x,y) {return x + y}
const predecessor = new Function("x", "return x - 1") // Don’t actually use this

const x = {
  f: (a) => a * 3,                  // arrow function
  g: function (a) {return a * 3},   // Non-arrow function
  h(a) {return a * 3}               // Non-arrow function
}

A function value is fully first-class in JavaScript meaning:

Furthermore a function is an object so:

Argument Passing

Passing an argument to a parameter is just like assignment. Nothing more, nothing less. But there are interesting things:

If you pass too many arguments, the extras are ignored:

function f(x, y, z) { return [x, y, z] }
f(3, 1, 8, 5, 9)                            // [3, 1, 8]

If you pass too few arguments, the extras are undefined:

function f(x, y, z) { return [x, y, z] }
f(3, 1)                                     // [3, 1, undefined]

Parameters can have defaults:

function f(x = 1, y, z = 3) { return [x, y, z] }
f()                    // [1, undefined, 3]
f(5)                   // [5, undefined, 3]
f(5, 1)                // [5, 1, 3]
f(5, 10, 13)           // [5, 10, 13]
f(undefined, 2)        // [1, 2, 3]
f(undefined, 2, 100)   // [1, 2, 100]

The last parameter can be a rest parameter:

function f(x, ...y) { return [x, y] }
f()               // [undefined, []]
f(1)              // [1, []]
f(1, 2)           // [1, [2]]
f(1, 2, 3)        // [1, [2, 3]]
f(1, 2, 3, 4, 5)  // [1, [2, 3, 4, 5]]

Since argument passing is just assignment, we can do destructuring:

function line({x1, y1, x2, y2, style='solid', thickness=1, color='black'}) {
  return `Drawing a ${color} ${style} line from (${x1},${y1}) to (${x2},${y2}) with weight ${thickness}`
}
line({x2: 5, x1: 4, color: 'green', y1: 6, y2: 10})
    // 'Drawing a green solid line from (4,6) to (5,10) with weight 1'

That’s very readable at the point of call! You know what you’re passing. You should do this in your code more often. People do not do this enough, imho.

Higher-Order Functions

Functions that accept functions as parameters, or that return functions, are called higher order functions. This should be no big deal. We had higher order functions in the 1950s in Lisp, but it took many decades for the whole world to realize how awesome they are.

const plusTen = x => x + 10
const squared = x => x * x
const twice = (f, x) => f(f(x))
twice(plusTen, 5)                                    // 25
twice(squared, 8)                                    // 4096
const compose = (f, g) => (x => g(f(x)))
const squareThenPlusTen = compose(squared, plusTen)
squareThenPlusTen(3)                                 // 19
compose(plusTen, squared)(5)                         // 225
`twice expects ${twice.length} arguments`            // 'twice expects 2 arguments'

Many array operations are higher-order:

const a = [3, 5, 2, 8, 21, 72]
a.map(x => x / 2)                   // [ 1.5, 2.5, 1, 4, 10.5, 36 ]
a.filter(x => x % 2 == 0)           // [ 2, 8, 72 ]
a.reduce((x, y) => x + y, 0)        // 111
a.every(x => x < 20)                // false
a.some(x => x < 25)                 // true
a.find(x => x > 7)                  // 8
a.findIndex(x => x > 7)             // 3
a.flatMap(x => [x, x])              // [3, 3, 5, 5, 2, 2, 8, 8, 21, 21, 72, 72]

You often see these used in a method-chaining style:

players
  .filter(p => p.ip > 200)
  .map(p => ({name: p.name.toUpperCase(), era: 9 * p.runsAllowed / p.ip}))
  .sort((p1, p2) => p1.era - p2.era)
  .slice(0, 10)
  .map(p => `<tr><td>${p.name}</td><td>${p.era}</td></tr>`)

Scope

The scope of a binding is the region of code where the binding is in force.

Variables declared with var are local to the innermost enclosing function. When declared with const or let, they are local to the innermost enclosing block.

Variables are visible within their scope (this includes blocks and functions nested within that scope) and are invisible outside. Using a let or const variable in its scope but before the declaration (i.e., within the temporal dead zone) throws a ReferenceError; using a var variable in its TDZ just gives you undefined:

var a = 1
let b = 2
// c, d, e, and f are not in scope here
function example() {
  console.log(c) // undefined
  // If we tried to use d here, it would throw a ReferenceError
  var c = 3
  let d = 4
  console.log(e) // undefined
  // If we tried to use f here, it would throw a ReferenceError
  if (true) {
    var e = 5
    let f = 6
    console.log(a, b, c, d, e, f)
  }
  console.log(e) // 5
  // f out of scope, using it would throw a ReferenceError
}
// Only a and b are in scope here; c, d, e and f are no longer in scope
example()

Here’s a major ramification of function scope versus block scope:

let a = []
for (var i = 0; i < 10; i++) { a[i] = () => i }
a[3]()                                              // 10, ASLKASJDASFKJASFLKAKLLJASDF
for (let i = 0; i < 10; i++) { a[i] = () => i }
a[3]()                                              // 3, YES I EXPECTED THAT
Exercise: Study that example.
NEVER USE var.

The var keyword is obsolete. Never use it. You should know how it works though, because you might see it in other people’s code.

Closures

When an inner function is sent outside its enclosing function, it is called a closure, because JavaScript retains the outer variables that live in a wrapping scope that “closes over” the function. Closures are crazy popular in JavaScript because local variables do information hiding.

You will often see code like this:

const nextFib = (() => {
  let [a, b] = [0, 1]
  return () => {
    [a, b] = [b, a + b]
    return a
  }
})()

nextFib()    // 1
nextFib()    // 1
nextFib()    // 2
nextFib()    // 3
nextFib()    // 5
nextFib()    // 8

Fun fact: the expression that we assigned to nextFib is called an IIFE (an acronym for immediately invoked function expression). Do you see why it is called that?

Generators

There’s another way to do that previous example. It’s a little weirder. Learn both ways.

function* fibGenerator() {
  let [a, b] = [0, 1]
  while (true) {
    [a, b] = [b, a + b]
    yield a
  }
}

const fib = fibGenerator()
fib.next()    // { value: 1, done: false }
fib.next()    // { value: 1, done: false }
fib.next()    // { value: 2, done: false }
fib.next()    // { value: 3, done: false }
fib.next()    // { value: 5, done: false }
fib.next()    // { value: 8, done: false }

The generator starts out suspended. When you invoke next() on it, it runs until a yield then suspends until you call next() again.

Generators don’t have to have infinite loops. You can have them end. Then they look good in for-loops. Example:

function* powerGenerator(base, limit) {
  let value = 1
  while (value <= limit) {
    yield value
    value *= base
  }
}

const twos = powerGenerator(2, 5)
twos.next()           // {value: 1, done: false}
twos.next()           // {value: 2, done: false}
twos.next()           // {value: 4, done: false}
twos.next()           // {value: undefined, done: true}
twos.next()           // {value: undefined, done: true}

for (let t of powerGenerator(3, 100)) {
  console.log(t)
}
// prints 1 3 9 27 81

There’s more to all this, so see the MDN Docs on this topic.

Currying

Check out these two functions:

const f = (x, y) => x + y    // Uncurried

const g = x => y => x + y    // Curried

Okay, so we call the first one like f(5, 8) and the second one like g(5)(8), but is there any other difference? Well, the second one admits partial application:

const add5 = g(5)  // Applying the “first” argument now
add5(12)           // and the “second” argument later
add5(100)          // We can use the partially applied function over and over
Exercise: Write a “chainable” function that accepts one string per call, but when called without arguments, returns the words previously passed, in order, separated by a single space. For example say('Hello')('my')('name')('is')('Colette')() should return the string 'Hello my name is Colette'.

This

JavaScript has a really wild way to handle context.

The expression this evaluates to different things based on what the heck is going on at run time.

  1. If used in a top-level statement outside of any function, or at the top-level statement in a function called as a global function, it refers the global object.
    a = 3
    this.a                      // 3
    this.a = 2
    a                           // 2
    function f() {this.a = 10}
    f()
    a                           // 10
    
  2. If used in a function produced through one of the methods apply, call, or bind, the value for this is the first argument of the method. Woah! (Note: there are other places in JavaScript where you can “pass in” a value for this, but these are the three main ones, perhaps.) This totally requires examples to understand:
    let a = {x: 1, y: 3}
    function f(c, s) {this.x = this.x * c + s}
    
    f.apply(a, [3, 5])
    a                           // { x: 8, y: 3 }
    f.call(a, 4, 1)
    a                           // { x: 33, y: 3 }
    f.bind(a)(3, 8)
    a                           // { x: 107, y: 3 }
    
  3. If called via the syntax obj.f() where f is a NON-ARROW function property of obj, then this refers to obj, which we call the receiver, while the function is called a method.
    let x = {a: 1, b: function (x) {return x + this.a}}
    x.b(2)                                    // 3
    
    // Wait we should use a more modern syntax
    let x = {a: 1, b(x) {return x + this.a}}
    x.b(2)                                    // 3
    
    // The expression is evaluated dynamically, not statically
    let f = function (x) {return x + this.a}
    let x = {a: 10, b: f}
    x.b(2)                                    // 12
    
    // Hey let's bundle up methods with our data!
    let p = {
      x: 0,
      y: 0,
      move(dx, dy) {this.x += dx; this.y += dy},
      reflect() {this.x = -this.x; this.y = -this.y}
    }
    p.move(5, 4)
    p.reflect()
    [p.x, p.y]      // [-5, -4]
    
  4. If used in a function invoked with the new operator, it refers to the object being created. We’ll get to this a little later.

Arrow vs. Non-Arrow Functions

Did you see that we pointed out that this is bound to the receiver only for non-arrow functions? That’s a big deal, since it means methods should never use arrows:

const counter = {
  val: 0,
  inc() { this.val++ },       // This does what you want
  dec: () => { this.val-- }   // NOOOOOOO THIS WON'T WORK
}

It’s a big deal, too, for functions nested within methods. You should use arrows for these, so that the this doesn’t get hijacked:

const counter = {
  val: 10,
  countDown() {
    if (this.val > 0) {
      setTimeout(() => {console.log(--this.val); this.countDown()}, 1000)
    }
  }
}
Exercise: What would happen if you replaced the first argument of setTimeout with function () {console.log(--this.val); this.countDown()}?
Remember, arrow functions don’t get this mapped to their receiver.

When using methods, i.e., functions inside an object that need to refer to the object, you want the function syntax. You can use the arrows everywhere else.

Constructors

The previous example made a point object containing data fields and methods. If we want to make millions of point objects, each one of them would have copies of the move and reflect functions. If you care about saving memory, we should put those functions in a prototype. Interestingly, Eich, way back in 1995, created an operator, called new, that facilitates a pattern of creating objects with a shared prototype. Here’s how it works:

function Point(x = 0, y = 0) {
  this.x = x
  this.y = y
}
Point.prototype.move = function (dx, dy) {this.x += dx; this.y += dy}
Point.prototype.reflect = function () {this.x = -this.x; this.y = -this.y}
Point.prototype.toString = function () {return `(${this.x},${this.y})`}

let p = new Point(3, 7)  // THE new IS CRUCIAL!

When you call the function with new, the function you call behaves as a constructor which means:

This works because of something interesting: every function you create has three properties: name, length, which holds the number of parameters for the function, and prototype, which is an object with a constructor property, whose value is the function itself.

jspoints.png

To summarize, we build the constructor, assigning properties to the new object which is referred to as this (provided the constructor is invoked with new). Define the methods in the prototype as non-arrow functions! You want them to be non-arrow functions so that this refers to the right object.

Class Syntax

There is a keyword class which you can use to build exactly the same structure we just built with functions.

class Point {
  constructor(x = 0, y = 0) {
    this.x = x
    this.y = y
  }
  move(dx, dy) {this.x += dx; this.y += dy}
  reflect() {this.x = -this.x; this.y = -this.y}
  toString() {return `(${this.x},${this.y})`}
}

let p = new Point(3, 7)  // THE new IS CRUCIAL!

This code creates a function called Point whose body is the constructor, and it assigns those three functions to Point.prototype. Keep in mind that using the class keyword is nothing more than syntactic sugar for functions and prototypes. There are no such thing as classes in JavaScript. Only functions.

Exercise: Try out this code. Evaluate typeof Point. You got "function", right?

Prototype Chains

The class syntax has a little feature to automatically make prototype chains, which is a way of doing inheritance. Here’s an example. We have three types of animals. All animals have a name. Each kind of animal makes a specific kind of sound. But no matter what kind of animal we have, we can have the animal speak by saying its name and making its sound:

animals.js
class Animal {
  constructor(name) { this.name = name }
  speak() { return `${this.name} says ${this.sound()}` }
}

class Cow extends Animal {
  sound() { return 'moooo' }
}

class Horse extends Animal {
  sound() { return 'neigh' }
  gallop() { return 'galloping woohoo' }
}

class Sheep extends Animal {
  sound() { return 'baaaa' }
}

const h = new Horse('CJ')
for (const a of [h, new Cow('Bessie'), new Sheep('Little Lamb')]) {
  console.log(a.speak())
}

The extends clause chains the prototypes together, and for any class that explicity extends another, the prototype of its constructor will be its “superclass”:

jsprotos.png

These so-called “deep class hierarchies” are frowned upon by some folks in the JavaScript community. But go ahead and use them if you know what you are doing and you have one of those rare cases where it makes sense for your application.

Private Properties

Fields and methods prefixed with a # are invisible outside the class. They are super useful for not-leaking-internal-representations (so users don’t make mistakes and so programmers can freely change their internal representations) and for simplifying the interface of an object:

class Queue {
  #data
  #validateNotFull() { if (this.size >= 256) throw new Error("Queue full") }
  #validateNotEmpty() { if (this.size <= 0) throw new Error("Queue empty") }
  constructor() { this.#data = [] }
  add(item) { this.#validateNotFull(); this.#data.push(item) }
  remove() { this.#validateNotEmpty(); return this.#data.shift() }
  peek() { this.#validateNotEmpty(); return this.#data[0] }
  get size() { return this.#data.length }
}

The properties #data, #validateNotEmpty, and #validateNotFull are completely inaccessible outside the class. They also don’t behave quite the same as regular properties. Just use them for class internals and don’t try to do anything fancy with them.

Static Properties

Sometimes you want a property that belongs to the class, not to each instance. You can just assign the property directly to the class object after you define it (kind of ugly), or use static to keep the definition right there with the class:

class Key {
  static prefix = 't'
  static #nextId = 0
  constructor(name) {
    this.id = Key.#nextId++
    this.prefix = Key.prefix
  }
  toString() { return `${this.prefix}_${this.id}`}
  static skip(n) {
    if (typeof n !== 'number' || n < 0 || n > 100) {
      throw new Error('Bad skip value')
    }
    Key.#nextId += n 
  }
}

let k = new Key()
console.log(k)                          // Key { id: 0, prefix: 't' }
console.log(new Key())                  // Key { id: 1, prefix: 't' }
console.log(new Key().toString())       // t_2
Key.prefix = 'Q'
console.log(new Key().toString())       // Q_3
console.log(new Key().toString())       // Q_4
console.log(new Key().toString())       // Q_5
Key.skip(82)
console.log(new Key().toString())       // Q_88

You can mix static/non-static with private/non-private any way you like.

Optionals

Sometimes you want to design for a case where certain properties may be required of an object and some may be optional. Optional data requires some care. Here’s an example: say you want to know the city that the top player’s coach lives in. You could write:

topPlayer.coach.address.city

But maybe there’s no top player. Or maybe the top player has no coach (null) or the coach is not known (undefined). Or maybe the coach’s address is unknown or missing. Or perhaps the address exists but there’s no city field was missing. If any of these cases hold, the expression above throws a TypeError. You could write:

topPlayer && topPlayer.coach && topPlayer.coach.address ? 
  topPlayer.coach.address.city :
  undefined

But it’s better to go with:

topPlayer?.coach?.address?.city

Here x?.y produces undefined is x is nullish, and x.y otherwise. This operator works for membership access with square brackets and with function calls, too:

a?.[i]            // undefined if a is nullish, else a[i]
f?(args)          // undefined if f is nullish, else f(args)

There‘s actually a caveat here: in x?.y (or x?.[y] or x?.(y)), if x were never declared, a ReferenceError will be thrown. But if x were explicitly given the value undefined, the whole expression will be undefined. This actually makes sense.

Regular Expressions

Like most modern languages, JavaScript has a great regular expression subsystem. Regular expressions are constructed by the RegExp function, and have the common slash-delimited syntax:

Regular expressions are a huge topic in themselves, so for details on JavaScript’s regular expressions see the MDN documentation or the section on regular expressions in the Modern JavaScript Tutorial.

Asynchronous Programming

JavaScript is generally used in asynchronous contexts. This is such a huge topic that my notes are on a separate page of its own.

You should also see The Asynchronous Programming section in the MDN JavaScript tutorials and the section on promises and async/await in the Modern JavaScript Tutorial and the Asynchronous Programming chapter in Haverbeke’s book.

Binary Data

When dealing with image, sound, and video data, you have these big objects containing raw bytes, called buffers. You can put a data view on the buffer so that you can treat those bytes as sequences of signed or unsigned blocks of 8, 16, 32, or 64-bits integers, or as 32 or 64-bit floating point values. Example:

const buffer = new ArrayBuffer(16)
const view1 = new Uint8Array(buffer)
for (let i = 0; i < 16; i++) view1[i] = i*16
view1                      // [0,16,32,48,64,80,96,112,128,144,160,176,192,208,224,240]
const view2 = new Int8Array(buffer)
view2                      // [ 0,16,32,48,64,80,96,112,-128,-112,-96,-80,-64,-48,-32,-16]
const view3 = new Uint16Array(buffer)
view3                      // [4096,12320,20544,28768,36992,45216,53440,61664]
new Int16Array(buffer)     // [4096,12320,20544,28768,-28544,-20320,-12096,-3872]
new Uint32Array(buffer)    // [807407616,1885360192,2963312768,4041265344]
new Int32Array(buffer)     // [807407616,1885360192,-1331654528,-253701952]
new Float32Array(buffer)   // [5.823039828101173e-10,2.7768663398802148e+29,-1.1682601552820415e-9,-5.566160437186068+29]
new Float64Array(buffer)   // [2.0261577571987577e+233,-5.34656515363983e+235]
new BigUint64Array(buffer) // [8097560366627688448n,17357102489901502592n]
new BigInt64Array(buffer)  // [8097560366627688448n,-1089641583808049024n]

Check out the MDN Overview and the MDN reference of typed arrays.

Metaprogramming

There is a lot of disagreement about what actually constitutes metaprogramming, but for these notes let’s call it operating on program constructs as opposed to operating on application-level objects. For example, programming would involve asking “how old is this person?” or “what is the shortest path from Los Angeles to Madrid?” and metaprogramming might ask “what are the properties of this object and which are writable?”, “change the prototype this object to something else,” or “manufacture some new code at runtime from a string and execute it.”

Object Introspection and Self-Modification

You can inquire about, access, and modify an object’s prototype:

let basicCircle = { x: 0, y: 0, r: 1 }
let c = Object.create(basicCircle)
c.x = 2
c.color = 'blue'
Object.getPrototypeOf(c)                           // basicCircle
Object.getPrototypeOf(basicCircle)                 // Object.prototype
Object.prototype.isPrototypeOf(basicCircle)        // true
basicCircle.isPrototypeOf(c)                       // true
let anotherCircle = { x: 10, y: 3 }
Object.setPrototypeOf(anotherCircle, basicCircle)  // but this is slow

You can inquire about, access, and modify an object’s properties:

Object.keys(c)                             // [ 'x', 'color' ]
Object.getOwnPropertyNames(c)              // [ 'x', 'color' ]
let a = []; for (let p in c) a.push(p); a  // [ 'x', 'color', 'y', 'r' ]
c.hasOwnProperty('color')                  // true
c.hasOwnProperty('y')                      // false

You can inquire about and change the way properties are allowed to be used:

Object.getOwnPropertyDescriptor(c, 'x')          // { value: 2,
                                                 //   writable: true,
                                                 //   enumerable: true,
                                                 //   configurable: true }
c.x = 10
c.x                                              // 10
Object.defineProperty(c, 'x', {writable: false})
Object.getOwnPropertyDescriptor(c, 'x')          // { value: 10,
                                                 //   writable: false,
                                                 //   enumerable: true,
                                                 //   configurable: true }
c.x = 500                                        // no error, but ...
c.x                                              // 10

Those things that are in the property descriptors are called attributes. The initial attribute values depend on how the object is created:

// Object literals have free, open, changeable properties
a = { x: 1, y: 2 }
Object.getOwnPropertyDescriptor(a, 'x')   // { value: 1,
                                          //   writable: true,
                                          //   enumerable: true,
                                          //   configurable: true }

// Object.create makes rather closed and locked-down properties
b = Object.create(Object.prototype, {x: { value: 1 }, y: { value: 2 }})
Object.keys(b)                            // []
Object.getOwnPropertyNames(b)             // [ 'x', 'y' ]
Object.getOwnPropertyDescriptor(b, 'x')   // { value: 1,
                                          //   writable: false,
                                          //   enumerable: false,
                                          //   configurable: false }

Important: A property can have either a data descriptor OR an access descriptor.

What do these mean?

AttributeDescription
valuecurrent value
writabletrue iff it can be updated
geta function to call when the property is accessed
seta function to call on attempt to set the property
enumerablewill it appear in a for-in and Object.keys?
configurablecan it be deleted and have its values other than writable changed?

Properties have data descriptors unless you define them with the get or set keywords. The usual example:

const c = {
  radius: 1,
  get area() { return Math.PI * this.radius * this.radius },
  set area(a) { this.radius = Math.sqrt(a / Math.PI) },
}

console.log(c)            // prints { radius: 1, area: [Getter/Setter] }
c.radius = 2              // It's writable
console.log(c.area)       // prints 4π (12.566370614359172)
c.area = 100 * Math.PI    // calls the setter
console.log(c.radius)     // prints 10

See the MDN page for defineProperty for great examples.

You can even change how an object itself is allowed to be modified:

MethodDescription
Object.preventExtensions(x)no new properties may be added to x
Object.seal(x)prevent extensions and make all properties nonconfigurable (can’t delete and can’t change any attributes except writable)
Object.freeze(x)seal and makes it non-writable too.

You can inquire which objects exist in your program. Just get a handle on the global object and list its properties.

Eval

Calling eval(s) evaluates the string s as JavaScript code.

Aside from a tiny number of legitimate, and very advanced, use cases, this function is a major security risk and never almost never be used. Before you try to use it, look for an alternative.

Read more about eval at Axel Rauschmayer’s blog.

Proxies

Read a fabulous overview by Axel Rauschmayer.

globalThis

You know something interesting? The so-called “global variables” in JavaScript are actual properties of a single global object. How’s that for object-oriented? You can access this one, cosmic, top, global, all-powerful object from where everything emanates with the expression globalThis. Do you know why that works? Because the global object has a property called globalThis which is a reference to itself! 🤯🤯

Exercise: Draw this state of affairs.
Exercise: What is the value of globalThis.globalThis.globalThis.globalThis === globalThis? Why?

What counts as a “global” variable? Top-level variables declared with var, or not declared at all.(That means top-level let and const variables do not go into the global object. Function declarations also generate global variable entries.

Modules

Roughly, a module is a file containing code that can be exported. Scripts (and other modules) can import code from a module and use it. Modules are pretty much just files in JavaScript.

Here’s a trivial example, featuring a stats module that exports functions mean and variance, with a sum function left private to the module:

stats.js
function sum(a) {
  return a.reduce((x, y) => x + y, 0)
}

export function mean(a) {
  return sum(a) / a.length
}

export function variance(a) {
  const m = mean(a)
  return sum(a.map(x => (x - m) ** 2)) / a.length
}

The main script imports both mean and variance and uses them.

main.js
import { mean, variance } from './stats.js'

const scores = [35, 99, 22, 57, 98, 75, 69, 88]
console.log(mean(scores), variance(scores))
To run this program, we’d have to either give the files .mjs file extensions, or include a file called package.json with a top level entry "type": "module". Let’s try the latter:

$ echo '{"type": "module"}' > package.json
$ node main.js
67.875 704.609375

The import/export syntax is incredibly flexible. See the MDN docs for export and the MDN docs for import to see just how flexible.

But really, for excellent overview about the module system, read Rauschmayer’s book chapter. You can also check out the MDN Guide to Modules in the Browser. You can even read this short tutorial, too.

Some Reference Material

Statements

A program is a sequence of statements and function declarations. Function declarations begin with function, function*, async function, async function*, or class. The statements are:

Semicolons

Here’s a quote from the official language specification:

Most ECMAScript statements and declarations must be terminated with a semicolon. Such semicolons may always appear explicitly in the source text. For convenience, however, such semicolons may be omitted from the source text in certain situations. These situations are described by saying that semicolons are automatically inserted into the source code token stream in those situations.

The spec goes on to state what those “Rules of Automatic Semicolon Insertion” are. What this means in practice is that you can write those semicolons yourself, or leave them out (at least as many as you can) and rely on ASI, as it’s known, to make things right, which it can only do if you know exactly what you are doing. Before you go the non-semicolon route (which is fine!) make sure you understand:

Good explanations, besides the official spec are this one, this one, this one, and this one.

Exercise: Ready to omit semicolons? Get your ASI Certification.
Pro Tip

Whether you are use semicolons or not, set up your development environment to use a code-formatter, like Prettier, that knows how to parse and format your code and enforce a style. The formatter will help a lot (but it doesn’t always read your mind perfectly; at the end of the day, you are responsible for writing correct code.

Keywords

Keywords are those words that may not be used as identifiers:

Future Keywords may be used in the future as keywords:

The following are essentially reserved due to static semantic restrictions in some contexts:

Operators

From highest to lowest precedence:

Precedence Description Structure Associativity
21 Grouping ( $\mathit{exp}$ ) N/A
20 Member Access $\mathit{exp}$ . $\mathit{prop}$ Left
Computed Member Access $e_1$ [ $e_2$ ] Left
Null-Safe Member Access $e_1$ ?. $e_2$ Left
Function Call $fun$ ( $\mathit{args}$ ) Left
new (with argument list) new $fun$ ( $\mathit{args}$ ) N/A
19 new (without argument list) new $fun$ N/A
18 Postfix Increment $\mathit{exp}$ ++ N/A
Postfix Decrement $\mathit{exp}$ --
17 Logical NOT ! $\mathit{exp}$ N/A
Bitwise NOT ~ $\mathit{exp}$
Unary Plus + $\mathit{exp}$
Unary Negation - $\mathit{exp}$
Prefix Increment ++ $\mathit{exp}$
Prefix Decrement -- $\mathit{exp}$
Type typeof $\mathit{exp}$
Void void $\mathit{exp}$
Delete property delete $\mathit{exp}$
Await await $\mathit{exp}$
16 Exponentiation $e_1$ ** $e_2$ Right
15 Multiplication $e_1$ * $e_2$ Left
Division $e_1$ / $e_2$
Remainder $e_1$ % $e_2$
14 Addition $e_1$ + $e_2$ Left
Subtraction $e_1$ - $e_2$
13 Bitwise Left Shift $e_1$ << $e_2$ Left
Bitwise Right Shift $e_1$ >> $e_2$
Bitwise Unsigned Right Shift $e_1$ >>> $e_2$
12 Less Than $e_1$ < $e_2$ Left
Less Than Or Equal $e_1$ <= $e_2$
Greater Than $e_1$ > $e_2$
Greater Than Or Equal $e_1$ >= $e_2$
Property Test $e_1$ in $e_2$
Instance Test $e_1$ instanceof $e_2$
11 Similarity $e_1$ == $e_2$ Left
Non-Similarity $e_1$ != $e_2$
Equality $e_1$ === $e_2$
Inequality $e_1$ !== $e_2$
10 Bitwise AND $e_1$ & $e_2$ Left
9 Bitwise XOR $e_1$ ^ $e_2$ Left
8 Bitwise OR $e_1$ | $e_2$ Left
7 Logical AND $e_1$ && $e_2$ Left
6 Logical OR $e_1$ || $e_2$ Left
5 Nullish Coalesce $e_1$ ?? $e_2$ Left
4 Conditional $e_1$ ? $e_2$ : $e_3$ Right
3 Assignment $e_1$ = $e_2$ Right
$e_1$ += $e_2$
$e_1$ -= $e_2$
$e_1$ **= $e_2$
$e_1$ *= $e_2$
$e_1$ /= $e_2$
$e_1$ %= $e_2$
$e_1$ <<= $e_2$
$e_1$ >>= $e_2$
$e_1$ >>>= $e_2$
$e_1$ &= $e_2$
$e_1$ ^= $e_2$
$e_1$ |= $e_2$
$e_1$ &&= $e_2$
$e_1$ ||= $e_2$
$e_1$ ??= $e_2$
2 Yield yield $\mathit{exp}$ Right
Delegate Yield yield* $\mathit{exp}$
1 Sequence $e_1$ , $e_2$ Left

Good to know:

Built-in Objects

JavaScript, the language, tracks the ECMAScript Scripting Language Specification, so all of the standard objects in the ECMAScript specification are available to your JavaScript programs. Here are those objects, with a representative set of their properties:

NamePrototypeProperties
The global
object
(unnamed)
Implemen-
tation
dependent
globalThis Infinity NaN undefined eval() isFinite() isNaN() parseFloat() parseInt() decodeURI() decodeURIComponent() encodeURI() encodeURIComponent() Math JSON Reflect Atomics Object() Function() Boolean() Number() BigInt() String() Symbol() Array() Float32Array() Float64Array() Int8Array() Int16Array() Int32Array() Uint8Array() Uint8ClampedArray() Uint16Array() Uint32Array() BigInt64Array() BigUint64Array() ArrayBuffer() SharedArrayBuffer() DataView() Date() RegExp() Set() WeakSet() Map() WeakMap() Proxy() Promise() Error() SyntaxError() RangeError() TypeError() ReferenceError() URIError() EvalError()
ObjectFunction.
prototype
prototype create() assign() is() getPrototypeOf() setPrototypeOf() defineProperties() defineProperty() getOwnPropertyDescriptor() getOwnPropertyDescriptors() getOwnPropertyNames() getOwnPropertySymbols() keys() values() entries() preventExtensions() isExtensible() seal() isSealed() freeze() isFrozen()
Object.prototypenullconstructor toString() toLocaleString() valueOf() hasOwnProperty() isPrototypeOf() propertyIsEnumerable()
MathObject.
prototype
E LN10 LN2 LOG2E LOG10E PI SQRT1_2 SQRT2 abs() acos() acosh() asin() asinh() atan() atanh() atan2() cbrt() ceil() clz32() cos() cosh() exp() expm1() floor() fround() hypot() imul() log() log1p() log10() log2() max() min() pow() random() round() sign() sin() sinh() sqrt() tan() tanh() trunc()
JSONObject.
prototype
parse() stringify()
ReflectObject.
prototype
getPrototypeOf() setPrototypeOf() construct() ownKeys() has() defineProperty() getOwnPropertyDescriptor() get() set() apply() deleteProperty() isExtensible() preventExtensions()
AtomicsObject.
prototype
add() and() compareExchange() exchange() isLockFree() load() or() store() sub() wait() wake() xor()
FunctionFunction.
prototype
length prototype
Function.prototypeObject.
prototype
constructor toString() apply() call() bind()
Function instancesFunction.
prototype
name length prototype
BooleanFunction.
prototype
prototype
Boolean.prototypeObject.
prototype
constructor toString() valueOf()
NumberFunction.
prototype
prototype NEGATIVE_INFINITY POSITIVE_INFINITY MIN_VALUE MAX_VALUE isFinite() EPSILON isInteger() MIN_SAFE_INTEGER MAX_SAFE_INTEGER isSafeInteger() NaN isNaN() parseInt() parseFloat()
Number.prototypeObject.
prototype
constructor toString() toLocaleString() valueOf() toFixed() toExponential() toPrecision()
BigIntFunction.
prototype
prototype asIntN() asUintN()
BigInt.prototypeObject.
prototype
constructor toString() toLocaleString() valueOf()
StringFunction.
prototype
prototype fromCharCode() fromCodePoint() raw()
String.prototypeObject.
prototype
constructor charAt() charCodeAt() codePointAt() startsWith() endsWith() includes() indexOf() lastIndexOf() slice() substring() toLowerCase() toUpperCase() toLocaleLowerCase() toLocaleUpperCase() normalize() localeCompare() trim() trimStart() trimEnd() padStart() padEnd() concat() repeat() match() matchAll() search() replace() split() toString() valueOf()
String instancesString.
prototype
length
SymbolFunction.
prototype
prototype hasInstance isConcatSpreadable iterator asyncIterator match replace search species split() toPrimitive toStringTag unscopables for() keyFor()
Symbol.prototypeObject.
prototype
constructor toString() valueOf()
RegExpFunction.
prototype
prototype
RegExp.prototypeObject.
prototype
constructor flags global ignoreCase unicode multiline sticky dotAll source test() exec() toString()
RegExp instancesRegExp.
prototype
lastIndex
DateFunction.
prototype
prototype parse() UTC() now()
Date.prototypeObject.
prototype
constructor getDate() getTime() getFullYear() getMonth() getDay() getHours() getMinutes() getSeconds() getMilliseconds() getTimezoneOffset() getUTCDate() getUTCFullYear() getUTCMonth() getUTCDay() getUTCHours() getUTCMinutes() getUTCSeconds() getUTCMilliseconds() setDate() setTime() setFullYear() setMonth() setHours() setMinutes() setSeconds() setMilliseconds() setUTCDate() setUTCFullYear() setUTCMonth() setUTCHours() setUTCMinutes() setUTCSeconds() setUTCMilliseconds() toString() toISOString() toUTCString() toDateString() toTimeString() toLocaleString() toLocaleDateString() toLocaleTimeString() toJSON() valueOf()
ArrayFunction.
prototype
prototype isArray() of() from()
Array.prototypeObject.
prototype
constructor keys() values() entries() includes() find() findIndex() indexOf() lastIndexOf() slice() every() some() forEach() map() filter() reduce() reduceRight() pop() push() shift() unshift() copyWithin() join() concat() fill() sort() reverse() splice() toLocaleString() toString()
Array instancesArray.
prototype
length
Int8Array
Uint8Array
Uint8ClampedArray
Int16Array
Uint16Array
Int32Array
Uint32Array
Float32Array
Float64Array
Function.
prototype
prototype BYTES_PER_ELEMENT of() from()
Int8Array.prototype
Uint8Array.prototype
Uint8ClampedArray.prototype
Int16Array.prototype
Uint16Array.prototype
Int32Array.prototype
Uint32Array.prototype
Float32Array.prototype
Float64Array.prototype
Object.
prototype
constructor BYTES_PER_ELEMENT buffer byteLength byteOffset length keys() values() entries() includes() find() findIndex() indexOf() lastIndexOf() slice() every() some() forEach() map() filter() reduce() reduceRight() copyWithin() join() fill() sort() reverse() set() subarray() toLocaleString() toString()
SetFunction.
prototype
prototype
Set.prototypeObject.
prototype
constructor size has() keys() values() entries() add() clear() delete() forEach()
ArrayBufferFunction.
prototype
prototype isView
ArrayBuffer.prototypeObject.
prototype
constructor byteLength slice()
SharedArrayBufferFunction.
prototype
prototype
SharedArrayBuffer.prototypeObject.
prototype
constructor byteLength slice()
DataViewFunction.
prototype
prototype
DataView.prototypeObject.
prototype
constructor buffer byteLength byteOffset getInt8() setInt8() getInt16() setInt16() getInt32() setInt32() getUint8() setUint8() getUint16() setUint16() getUint32() setUint32() setFloat64() setFloat32() getFloat64() getFloat32()
WeakSetFunction.
prototype
prototype
WeakSet.prototypeObject.
prototype
constructor has() add() has() delete()
MapFunction.
prototype
prototype
Map.prototypeObject.
prototype
constructor size has() get() set() keys() values() entries() clear() delete() forEach()
WeakMapFunction.
prototype
prototype
WeakMap.prototypeObject.
prototype
constructor has() get() set() delete()
ProxyFunction.
prototype
revocable()
Proxy.prototypeObject.
prototype
PromiseFunction.
prototype
prototype all() race() resolve() reject()
Promise.prototypeObject.
prototype
constructor then() catch()
ErrorFunction.
prototype
prototype
Error.prototypeObject.
prototype
constructor name message toString()
EvalError
RangeError
ReferenceError
SyntaxError
TypeError
URIError
Function.
prototype
prototype
EvalError.prototype
RangeError.prototype
ReferenceError.prototype
SyntaxError.prototype
TypeError.prototype
URIError.prototype
Error.
prototype
constructor name message

JavaScript in Practice

There’s JavaScript the language, and then there’s all the culture around it, which is pretty much inseparable. To use JavaScript in real life, this cultural awareness is crucial.

Versions of JavaScript

What people call the different “versions” of JavaScript vary a little bit, but it’s a good bet people agree on these versions:

VersionReleasedNotable New Features
ES31999(Let’s just consider this the baseline, anything before ES3 is, um, ancient history)
ES52009JSON, Getter and setter properties, Array.isArray, Array#{indexOf, every, some, forEach, map, filter, reduce, reduceRight}, Tons of methods on Object, String#{split, trim}, Function#bind, Many language fixes surrounding immutability and typing and this, Strict mode
ES2015 (ES6)2015let, const, class, super, function*, arrow functions, default parameters, rest parameters, destructuring, for-of, spreads, octal and binary literals, template literals (backtick strings and interpolation), Unicode escapes (\u{...}), computed properties, shorthand properties, RegExp y and u flags, promises, symbols, typed arrays, maps, sets, proxies, tons of new properties on built-in objects, and much more
ES2016 (ES7)2016Exponentiation (**), Array#includes
ES2017 (ES8)2017async/await, String#padStart, String#padEnd, Object.values, Object.entries, Object.getOwnPropertyDescriptors, SharedArrayBuffers, Atomics, RegExp u flag
ES2018 (ES9)2018Rest and spread (...) for object properties, Async iterators, for-await-of, Promise#finally, RegExp lookbehind, RegExp \p{} and \P{}, RegExp named capture groups, RegExp s flag
ES2019 (E10)2019Object.fromEntries, Array#flat, Array#flatMap, String#trimStart, String#trimEnd, Symbol#description, catch without an exception variable, Array#sort now stable
ES2020 (ES11)2020BigInt, Optional Chaining (?.), Nullish Coalescing (??), globalThis, String#matchAll, await import, Promise.allSettled, export *, import.meta, for-in defined order
ES2021 (ES12)2021Numeric literal separators, Promise.any, Logical assignment (&&= ||=, ??=), AggregateError, String#replaceAll, WeakRef, FinalizationRegistry, Array#sort defined order
ES2022 (ES13)2022Private fields and methods, Static fields and methods

Check Kangax's Compatibility Tables to see which implementations support which features of the different versions. Note the radical differences!

Host Objects

JavaScript programs run inside a host environment, which exposes its own global objects. Most web browser environments provide:

On the web, all of the elements (including images and video), attributes, and other pieces of the a web page are part of the Document Object Model or DOM. These are all visible to JavaScript, so most JavaScript programs essentially do graphics by manipulating the DOM (as in the BMI script at the beginning of these notes).

Graphics

I have a separate page of notes for this.

Server Side

Node.js is the dominant choice here. Understanding and using and mastering Node is a topic all in itself! It’s a system built on top of the V8 JavaScript Engine. It provides its own global variables, including:

console Intl escape unescape FinalizationRegistry WeakRef WebAssembly global process Buffer URL URLSearchParams TextEncoder TextDecoder clearInterval clearTimeout setInterval setTimeout queueMicrotask clearImmediate setImmediate module require _ _error util

And it comes with a number of built-in modules, including:

assert async_hooks buffer child_process cluster console constants crypto dgram dns domain events fs http http2 https inspector module net os path perf_hooks process punycode querystring readline repl stream string_decoder timers tls trace_events tty url util v8 vm wasi worker_threads zlib

And it hosts an ecosystem of packages, called npm, the largest collection of open source packages in the world (by far).

Libraries and Frameworks

Most serious JavaScript is done with third-party libraries that are not only full of functionality, like special effects (fading, animation, etc.) but that wonderfully hide all the cross-browser differences from the programmer. Then there are the frameworks, gazillions of them.

Probably the major JavaScript libraries and frameworks are:

Somewhat related to libraries and frameworks are tools and products many JavaScript developers use frequently. You’d do well to know about:

Some History

Here’s a quick, useful, and fun history of the language up until 2019:

Going farther back in time, Eich gave a talk in 2010 with the history of the language up to that point, with some thoughts on where the language was going. Many of the ideas never came to pass, but it’s great to see the history to understand the tradeoffs that were made to get to where we are today:

Related Languages

First of all, let’s get this out of the way: JavaScript is not like the other language with a similar name.

JavaScript is a subset of TypeScript.

Hundreds of languages compile to JavaScript. YES, HUNDREDS!!!

The JavaScript subset known as asm.js used to be a thing.

Web Assembly is related, in a way, to JavaScript.

Wat

JavaScript is awesome, for the most part, but sometimes you need to laugh at its faults. Watch Gary Bernhardt’s famous Wat talk.