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:
You can run JavaScript programs (scripts) from the command line or directly within a web browser.
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:
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.
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.
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!
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.
All values belong to one of 8 types:
Type | Values of the Type |
---|---|
Undefined | Only one value: undefined . Means “I don’t know,” “I don’t care”, or “None of your business.”
|
Null | Only one value: null . Means “no value.”
|
Boolean | Only two values: true and false .
|
Number | The 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:
|
BigInt | Arbitrary-precision integers. Needed because the Number type can not represent most integers with a magnitude above 9007199254740992. Examples:
|
String | Immutable sequences of zero or more UTF-16 code units. You can delimit them with apostrophes, quotation marks, or backticks. Examples:
|
Symbol | Unique 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:
|
Object | Everything that isn’t one of the above types. Examples:
|
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
x !== y
!(x === y)
x == y
x != y
!(x == y
)x && y
x || y
!x
x ?? y
null
or undefined
) then y, else xIn 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.
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:
undefined
⟶ false
null
⟶ false
0
⟶ false
NaN
⟶ false
true
false
true
true
true
When a number is expected:
undefined
⟶ NaN
null
⟶ 0
false
⟶ 0
true
⟶ 1
0
0
NaN
.valueOf()
When a string is expected:
undefined
⟶ "undefined"
null
⟶ "null"
false
⟶ "false"
NaN
⟶ "NaN"
toString()
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.)
9.3
, 0
, [0]
, false
, true
, ""
, "${''}"
, `${''}`
, 9.3
, []
, [[]]
, {}
.
JavaScript isn’t 100% weakly-typed thoughThere are some occasions where a
TypeError
occurs, such as using a symbol where a number or string is expected, trying to access a property onnull
orundefined
, and mixing numbers with bigints.
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 withvar
, 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.
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:
This is worth repeatingVariables 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
}
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:
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.
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.
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.
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 withx
andy
properties, you could sprinklepoint.x
andpoint.y
throughout your code. But if you wrotelet { x, y } = point
, you can just usex
andy
. You’ve destructured the point object into its components.
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
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:
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!
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
?
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?
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
splice
. Show that you can use it for inserting elements only, deleting elements only, or replacing elements only.
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 NodeList
s and HTMLCollection
s 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 operationsfind
,findIndex
,map
,filter
,reduce
,every
, andsome
will be discussed later.
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:
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.
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>`)
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
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.
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?
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.
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
say('Hello')('my')('name')('is')('Colette')()
should return the string 'Hello my name is Colette'
.
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.
a = 3
this.a // 3
this.a = 2
a // 2
function f() {this.a = 10}
f()
a // 10
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 }
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]
new
operator, it refers to the object being created. We’ll get to this a little later.
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)
}
}
}
setTimeout
with function () {console.log(--this.val); this.countDown()}
?
Remember, arrow functions don’t getthis
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.
Sometimes when I’m writing Javascript I want to throw up my hands and say “this is bullshit!” but I can never remember what “this” refers to
— Ben Halpern (@bendhalpern) March 20, 2015
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
expression in the function.
prototype
property of the function.
return this
at the end.
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.
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.
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.
typeof Point
. You got "function"
, right?
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:
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”:
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.
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.
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.
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.
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:
/dog/
/JavaScript/i
/mikasa|eren|armin/i
/colo(u)?r/
/<([^>]*)>[^<]*<\/\1>/
/\d{5}(-\d{4})?/
/[\p{L}’']+/gu
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.
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.
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.
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.”
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.
value
, writable
, enumerable
, configurable
get
, set
, enumerable
, configurable
What do these mean?
Attribute | Description |
---|---|
value | current value |
writable | true iff it can be updated |
get | a function to call when the property is accessed |
set | a function to call on attempt to set the property |
enumerable | will it appear in a for-in and Object.keys ? |
configurable | can 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:
Method | Description |
---|---|
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.
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.
Read a fabulous overview by Axel Rauschmayer.
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! 🤯🤯
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.
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:
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.
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.
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:
var
let
const
for
for...in
for...of
for await...of
while
do...while
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.
Pro TipWhether 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.
[God creating JavaScript]
— Neckbeard Hacker (@NeckbeardHacker) August 24, 2016
GOD: It uses prototype-based inheritance.
Angel: Nice.
GOD: Also it secretly adds semicolons to ur code.
A: wat
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:
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:
==
operator should never be used, as it is really hard to understand all the weird conditions, e.g. null == undefined
is true, but null === undefined
is false. And ==
is NOT transitive! "16" == 16
and 16 == "0x10"
but "0x10" != "16"
.
delete
sets its operand, which could be an object, property, or array element to undefined if it succeeds, or simply returns false if it does not (things declared with var
, let
, or const
or predefined entities cannot be deleted)
in
operator works with properties, not values, so 2 in ["A", "B", "C"]
is true but "C" in ["A", "B", "C"]
is false
p in x
returns true if x.p
was explicitly set to undefined, but false if p
was deleted (via delete x.p
).
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:
Name | Prototype | Properties |
---|---|---|
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() WeakRef() Proxy() Promise() Error() SyntaxError() RangeError() TypeError() ReferenceError() URIError() EvalError() AggregateError() FinalizationRegistry() |
Object | Function. prototype | prototype create() assign() fromEntries() is() getPrototypeOf() setPrototypeOf() defineProperties() defineProperty() getOwnPropertyDescriptor() getOwnPropertyDescriptors() getOwnPropertyNames() getOwnPropertySymbols() hasOwn() keys() values() entries() groupBy() preventExtensions() isExtensible() seal() isSealed() freeze() isFrozen() |
Object.prototype | null | constructor toString() toLocaleString() valueOf() hasOwnProperty() isPrototypeOf() propertyIsEnumerable() |
Math | Object. 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() |
JSON | Object. prototype | parse() stringify() |
Reflect | Object. prototype | getPrototypeOf() setPrototypeOf() construct() ownKeys() has() defineProperty() getOwnPropertyDescriptor() get() set() apply() deleteProperty() isExtensible() preventExtensions() |
Atomics | Object. prototype | add() and() compareExchange() exchange() isLockFree() load() or() store() sub() wait() waitAsync() notify() xor() |
Function | Function. prototype | length prototype |
Function.prototype | Object. prototype | constructor toString() apply() call() bind() |
Function instances | Function. prototype | name length prototype |
Boolean | Function. prototype | prototype |
Boolean.prototype | Object. prototype | constructor toString() valueOf() |
Number | Function. 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.prototype | Object. prototype | constructor toString() toLocaleString() valueOf() toFixed() toExponential() toPrecision() |
BigInt | Function. prototype | prototype asIntN() asUintN() |
BigInt.prototype | Object. prototype | constructor toString() toLocaleString() valueOf() |
String | Function. prototype | prototype fromCharCode() fromCodePoint() raw() |
String.prototype | Object. prototype | constructor isWellFormed() at() charAt() charCodeAt() codePointAt() startsWith() endsWith() includes() indexOf() lastIndexOf() slice() substring() toLowerCase() toUpperCase() toLocaleLowerCase() toLocaleUpperCase() toWellFormed() normalize() localeCompare() trim() trimStart() trimEnd() padStart() padEnd() concat() repeat() match() matchAll() search() replace() replaceAll() split() toString() valueOf() |
String instances | String. prototype | length |
Symbol | Function. prototype | prototype hasInstance isConcatSpreadable iterator asyncIterator match matchAll replace search species split toPrimitive toStringTag unscopables for() keyFor() |
Symbol.prototype | Object. prototype | constructor toString() valueOf() description |
RegExp | Function. prototype | prototype |
RegExp.prototype | Object. prototype | constructor flags global ignoreCase unicode unicodeSets multiline sticky dotAll hasIndices source test() exec() toString() |
RegExp instances | RegExp. prototype | lastIndex |
Date | Function. prototype | prototype parse() UTC() now() |
Date.prototype | Object. 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() |
Array | Function. prototype | prototype isArray() of() from() |
Array.prototype | Object. prototype | constructor keys() values() entries() includes() at() find() findIndex() findLast() findLastIndex() indexOf() lastIndexOf() slice() flat() every() some() forEach() map() filter() reduce() reduceRight() flatMap() toReversed() toSorted() toSpliced() with() pop() push() shift() unshift() copyWithin() join() concat() fill() sort() reverse() splice() toLocaleString() toString() |
Array instances | Array. prototype | length |
Int8Array Uint8Array Uint8ClampedArray Int16Array Uint16Array Int32Array Uint32Array BigInt64Array BigUint64Array Float32Array Float64Array | Function. prototype | prototype BYTES_PER_ELEMENT of() from() |
Int8Array.prototype Uint8Array.prototype Uint8ClampedArray.prototype Int16Array.prototype Uint16Array.prototype Int32Array.prototype Uint32Array.prototype BigInt64Array.prototype BigUint64Array.prototype Float32Array.prototype Float64Array.prototype | Object. prototype | constructor BYTES_PER_ELEMENT buffer byteLength byteOffset length keys() values() entries() includes() at() find() findIndex() findLast() findLastIndex() indexOf() lastIndexOf() slice() every() some() forEach() map() filter() reduce() reduceRight() copyWithin() join() toReversed() toSorted() toSpliced() with() fill() sort() reverse() set() subarray() toLocaleString() toString() |
Set | Function. prototype | prototype |
Set.prototype | Object. prototype | constructor size has() keys() values() entries() add() clear() delete() forEach() |
ArrayBuffer | Function. prototype | prototype isView |
ArrayBuffer.prototype | Object. prototype | constructor byteLength detached resizable maxByteLength slice() resize() transfer() transferToFixedLength() |
SharedArrayBuffer | Function. prototype | prototype |
SharedArrayBuffer. prototype | Object. prototype | constructor byteLength growable maxByteLength slice() grow() |
DataView | Function. prototype | prototype |
DataView.prototype | Object. prototype | constructor buffer byteLength byteOffset getInt8() setInt8() getInt16() setInt16() getInt32() setInt32() getBigInt64() setBigInt64() getUint8() setUint8() getUint16() setUint16() getUint32() setUint32() getBigInt64() setBigInt64() getFloat32() setFloat32() getFloat64() setFloat64() |
WeakSet | Function. prototype | prototype |
WeakSet.prototype | Object. prototype | constructor has() add() has() delete() |
Map | Function. prototype | prototype |
Map.prototype | Object. prototype | constructor size has() get() set() keys() values() entries() clear() delete() forEach() |
WeakMap | Function. prototype | prototype |
WeakMap.prototype | Object. prototype | constructor has() get() set() delete() |
Proxy | Function. prototype | revocable() |
Proxy.prototype | Object. prototype | |
Promise | Function. prototype | prototype all() allSettled() any() race() resolve() reject() withResolvers() |
Promise.prototype | Object. prototype | constructor then() catch() |
WeakRef | Function. prototype | prototype |
WeakRef.prototype | Object. prototype | constructor deref() |
FinalizationRegistry | Function. prototype | prototype |
FinalizationRegistry. prototype | Object. prototype | constructor register() unregister() |
Error | Function. prototype | prototype |
Error.prototype | Object. prototype | constructor name message toString() |
EvalError RangeError ReferenceError SyntaxError TypeError URIError AggregateError | Function. prototype | prototype |
EvalError.prototype RangeError.prototype ReferenceError.prototype SyntaxError.prototype TypeError.prototype URIError.prototype AggregateError.prototype | Error. prototype | constructor name message |
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.
What people call the different “versions” of JavaScript vary a little bit, but it’s a good bet people agree on these versions:
Version | Released | Notable New Features |
---|---|---|
ES3 | 1999 | (Let’s just consider this the baseline, anything before ES3 is, um, ancient history) |
ES5 | 2009 | JSON , 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) | 2015 | let , 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) | 2016 | Exponentiation (** ), Array#includes |
ES2017 (ES8) | 2017 | async/await , String#padStart , String#padEnd , Object.values , Object.entries , Object.getOwnPropertyDescriptors , SharedArrayBuffers , Atomics , RegExp u flag |
ES2018 (ES9) | 2018 | Rest 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) | 2019 | Object.fromEntries , Array#flat , Array#flatMap , String#trimStart , String#trimEnd , Symbol#description , catch without an exception variable, Array#sort now stable |
ES2020 (ES11) | 2020 | BigInt , Optional Chaining (?. ), Nullish Coalescing (?? ), globalThis , String#matchAll , await import , Promise.allSettled , export * , import.meta , for-in defined order |
ES2021 (ES12) | 2021 | Numeric literal separators, Promise.any , Logical assignment (&&= , ||= , ??= ), AggregateError , String#replaceAll , WeakRef , FinalizationRegistry , Array#sort defined order |
ES2022 (ES13) | 2022 | Private fields and methods, Static fields and methods, Class static initialization blocks, at() for arrays, strings, and typed arrays, Error.cause , Regular expression d flag |
ES2023 (ES14) | 2023 | Array prototype methods findLast , findLastIndex , toReversed , toSorted , toSpliced , with |
ES2024 (ES15) | 2024 | Regular expresion v flag, new ArrayBuffer methods |
Check Kangax's Compatibility Tables to see which implementations support which features of the different versions. Note the radical differences!
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).
I have a separate page of notes for this.
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).
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:
Here’s a quick, useful, and fun history of the language up until 2019:
Here’s another one:
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, but not any more.
Web Assembly is related, in a way, to JavaScript.
JavaScript is awesome, for the most part, but sometimes you need to laugh at its faults. Watch Gary Bernhardt’s famous Wat talk.
Perhaps the biggest contributors to the things we make fun of JavaScript about were the (1) Make it look like Java mandate and the (2) Get it done in 10 days mandate. Eich is often asked what would be do over if he had the chance. Some of his replies:
Counterfactual alert, but see https://t.co/BN0YawTAm2.
— BrendanEich (@BrendanEich) June 23, 2023
If not typed language, obvious stuff:
- Shorter function keyword (fn wins).
- Nil only, no undefined and null.
- No primitives vs reference types Java trash.
- Option runtime type w/ match expression.
- Expression language!
If I didn't have "Make it look like Java" as an order from management, *and* I had more time (hard to unconfound these two causal factors), then I would have preferred a Self-like "everything's an object" approach: no Boolean, Number, String wrappers. No undefined and null. Sigh.
— BrendanEich (@BrendanEich) June 14, 2020
Just one nil, and no primitives vs references junk.
— BrendanEich (@BrendanEich) June 14, 2020
Note how Eich calls the primitives vs references distinction both “trash” and “junk”.