JavaScript

Arrays

Warning

Remember that sort, reverse, and splice mutate the original array! (see #Non-mutating methods)

Tip

If you're storing unique values, use a Set instead! Sets allow for lookups faster than linear (O(n)) time.

Filter out falsy values

array.filter(Boolean)

Create an array with a range of numbers

// [0, 1, 2, 3, 4]
[...Array(5).keys()]
// [1, 2, 3, 4, 5]
[...Array(5).keys()].map(i => i + 1)

Visualize sort function return values

   a?   b   a?
<---|---|---|--->
   -1   0   1

Sort strings with locale-awareness

people.sort((a, b) => a.name.localeCompare(b.name))

Sort elements based on a computed value (Schwartzian transform)

// example: sort files by modified time (earliest to latest)
// assume calculating the modified time is somewhat expensive

// ⛔️ this will recalculate the modified time on each comparison!
files.sort((a, b) => a.modifiedAt() - b.modifiedAt())

// instead, calculate the modified times once and store them temporarily
files = files.map(file => ([file, file.modifiedAt()]))
             .sort((a, b) => a[1] - b[1])
             .map(([file, modifiedAt]) => file)

Swap two elements using destructuring

;[arr[3], arr[5]] = [arr[5], arr[3]]

Shuffle (in place)

function shuffleArray(array) {
	for (let i = array.length - 1; i > 0; i--) {
		const j = Math.floor(Math.random() * (i + 1))
		;[array[i], array[j]] = [array[j], array[i]]
	}
}
const arr = ['apple', 'banana', 'orange', 'mango']
console.log(arr.at(1)) // banana
console.log(arr.at(-1)) // mango

Non-mutating methods

const foo = [1, 2, 3]
foo[1] = 4
console.log(foo) // [1, 4, 3]

const bar = [1, 2, 3]
const xyz = bar.with(1, 4)
console.log(bar) // [1, 2, 3]
console.log(xyz) // [1, 4, 3]

Language-aware sorting with Intl.Collator

console.log(['Z', 'a', 'z', 'ä'].sort(
    new Intl.Collator('de').compare)
);
// Expected output: Array ["a", "ä", "z", "Z"]

Language-aware list formatting with Intl.ListFormat

const fruit = ['apples', 'bananas', 'pears']

const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' })

formatter.format(fruit) // "apples, bananas, and pears"

Objects

Return an object from an an arrow function expression

// this won't work, because {} creates a block for the function body
const getObject = () => { a: 1, b: 2 }

// but this does
const getObject2 = () => ({ a: 1, b: 2 })

Destructuring

Destructure nested objects

const user = {
	first_name: 'Bob',
	address: {
		street: '123 Fake St',
		city: 'New York',
		state: 'NY'
	}
}

const {
	first_name,
	address: {
		street: address_street // renamed
	} = {} // default value to avoid error if address is undefined
} = user
// this will still error if address is null!

console.log(address_street)

Assign default values during destructuring

const foo = {
	a: 'orange',
}
const { a = 'apple', b = 'banana' } = foo

console.log(a, b) // 'orange banana'

Destructuring assignment to existing variables

let name, address
;({ name, address } = person)

Options objects with defaults

function test({ name = 'banana', color = 'yellow' } = {}) {
	console.log(`${name}s are ${color}`)
}

test({ name: 'apple', color: 'red' })  // apples are red
test({ name: 'lime' })                 // limes are yellow
test({})                               // bananas are yellow
test()                                 // bananas are yellow
function test2(options = { name: 'banana', color: 'yellow' }) {
	console.log(`${options.name}s are ${options.color}`)
}

test2({ name: 'apple', color: 'red' }) // apples are red
test2({ name: 'lime' })                // limes are undefined (errors in TS)
test2({})                              // undefineds are undefined (errors in TS)
test2()                                // bananas are yellow

Deep clone objects with structuredClone

Numbers

Separators

const a = 100000000
const b = 100_000_000

a === b // true

Random number in range (exclusive)

Math.random() * (max - min) + min;

Random integer in range (inclusive)

// assuming min and max are integers
return Math.floor(Math.random() * (max - min + 1) + min)

// if min is 0 this simplifies to
return Math.floor(Math.random() * (max + 1))

Strings

Use a function with String.replace

string.replace(
    /(hello)\s+(world)/,
    (match, p1, p2, /*...pN,*/ offset, string, groups) => {
    // p1, p2, etc. are capture groups, as many as there are in the regex
    // groups is an object with the named capturing groups, or undefined if there are none
    
    // do whatever you want here
    return replacement
})

Splice function for strings

function spliceString(input, start = 0, deleteCount = 0, additional = '') {
  return input.slice(0, start) + additional + input.slice(start + deleteCount)
}

Copy text to clipboard

await navigator.clipboard.writeText('text')

DOM

Element.closest

<div class="apple">
    <div class="orange outer">
        <div class="orange inner"></div>
        <div class="banana"></div>
    </div>
</div>
document.querySelector('.banana').closest('.orange')
// matches .orange.outer (.orange.inner isn't in the element's ancestor tree)

document.querySelector('.banana').closest('.banana')
// matches the element it was called on

Get the previous and next siblings

<div class="first"></div>
<div class="second"></div>
<div class="third"></div>
const element = document.querySelector('third')
element.previousElementSibling // <div class="second">
element.nextElementSibling // null

Insert DOM elements

<div id="target">
    <div>Existing content</div>
</div>
const before = document.createElement('div')
before.textContent = 'Before existing'

const after = document.createElement('div')
after.textContent = 'After existing'

const target = document.querySelector('#target')
target.prepend(before, 'Some text')
target.append(after)
<div id="target">
    <div>Before existing</div>
    Some text
    <div>Existing content</div>
    <div>After existing</div>
</div>
const heading = document.createElement('h2')
heading.textContent = 'Section heading'

const paragraph = document.createElement('p')
paragraph.textContent = 'Some descriptive text'

const target = document.querySelector('#target')
target.before(heading, paragraph)
target.after('This is a text node')
<h2>Section heading</h2>
<p>Some descriptive text</p>
<div id="target">Target element</div>
This is a text node
const outer = document.createElement('div')
outer.textContent = 'Outer div'
const inner = document.createElement('div')
inner.textContent = 'Inner div'

const target = document.querySelector('#target')
target.insertAdjacentElement('beforebegin', outer)
target.insertAdjacentElement('afterbegin', inner)
target.insertAdjacentHTML('beforeend', '<div>From HTML string</div>')
target.insertAdjacentText('afterend', 'Text node')
<div>Outer div</div>
<div id="target">
    <div>Inner div</div>
    Target node
    <div>From HTML string</div>
</div>
Text node

Find focused element

Use document.activeElement to find the currently focused element. You can add this as a live expression in the Chrome devtools to get a live updating view of the focused element.

Element heights/widths

ResizeObserver

const observer = new ResizeObserver((entries) => {
    entries.forEach(entry => {
        console.log('width', entry.contentBoxSize[0].inlineSize)
        console.log('height', entry.contentBoxSize[0].blockSize)
    })
})
observer.observe(document.querySelector('main'), {
    box: 'border-box'
})

DOMContentLoaded vs. load

DOMContentLoaded vs. load

The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.

The DOMContentLoaded event fires when the HTML document has been completely parsed, and all deferred scripts (<script defer src="…"> and <script type="module">) have downloaded and executed. It doesn't wait for other things like images, subframes, and async scripts to finish loading.

DOMContentLoaded does not wait for stylesheets to load, however deferred scripts do wait for stylesheets, and DOMContentLoaded queues behind deferred scripts. Also, scripts which aren't deferred or async (e.g. <script>) will wait for already-parsed stylesheets to load.

Styling

Media queries

const mediaQuery = window.matchMedia("(orientation: portrait)")
const isPortrait = mediaQuery.matches
mediaQuery.addEventListener('change', adjustLayout)

Restart CSS animation

element.classList.remove('animated')
element.offsetHeight
element.classList.add('animated')

Get computed style of pseudo-elements

<span class="temperature">73</span>
.temperature::after {
    content: 'ºF';
    color: gray;
}
const temp = document.querySelector('.temperature')
getComputedStyle(temp, '::after').color // rgb(128, 128, 128)

Language

Operator precedence

Assignment using switch statements

const screenName = (() => {
	switch (index) {
		case 0:
			return 'Home'
		case 1:
			return 'Profile'
		case 2:
			return 'Settings'
		default:
			return 'Unknown'
	}
})()

Test expressions in switch statements

switch(true) {
    case !user:
        throw new Error('User not provided')
    case !user.name:
        throw new Error('User name not provided')
    case user.name.length < 5:
        throw new Error('User name must be at least 5 characters')
    case typeof user.name !== 'string':
        throw new Error('User name is not a string')
}

Debugging

Quickly log variables with labels

Use object property shorthand to quickly log variables along with their names

const name = 'Sam'
const age = 32

console.log({ name, age }) // logs {name: 'Sam', age: 32}

Snapshot objects at the time of logging

When objects logged to the console are expanded, they reflect their current values. To preserve the value at the time of logging, duplicate the object by stringifying and parsing it.

const person = { name: 'Sam', age: 32 }

// This will show Sam in the inline log, but when expanded it will show Bill
console.log(person)
// This will show Sam even after expanding
console.log(JSON.parse(JSON.stringify(person)))

person.name = 'Bill'

Log objects with indentation

You can pass '\t' as the third argument to indent with tabs, or other values like dashes

console.log(JSON.stringify(object, null, '  '))

Log objects or arrays as a table

console.table({ foo: 'bar', obj: { a: 1, b: 2 }})
(index) Value a b
foo 'bar'
obj 1 2

Launch debugger with a countdown

setTimeout(() => { debugger }, 3000)

Console timers

function longRunningFunction() {
    const timerName = '[timer] longRunningFunction'
    console.time(timerName)
    doSomething()
    console.timeLog(timerName)
    doSomethingElse()
    console.timeEnd(timerName)
}

Copy from the console

Use the copy() function in the console to copy long strings from the console.

Other

Sleep function

async function sleep(time) {
    return await new Promise(resolve => window.setTimeout(resolve, time))
}

Sharing with the system share sheet

Warning

As of January 2025 not supported in Firefox, or Chrome on macOS and Linux

/* at least one pr operty should be specified */
const data: ShareData = {
  url: location.href,
  text: 'Some arbitrary text to share',
  title: 'Cool Web Site',
  // files: [/* some File objects */],
}

if (navigator.canShare?.(data)) {
  navigator.share(data)
}

error.cause

try {
  connectToDatabase();
} catch (err) {
  throw new Error("Connecting to database failed.", { cause: err });
}

JSDoc

Type variables and functions

/** @type {string} */
let upperName

/**
 * Makes the input uppercase
 * @param {string} input
 * @returns {string}
 */
function uppercase(input) {
  return input.toUpperCase()
}
/**
@param {string} param1 Description goes here
@param {boolean} [param2] This param is optional
@param {boolean} [param3=true] This param has a default value
*/
const photo = /** @type {HTMLImageElement} */ (
    document.querySelector('#photo)
)

Tuples

/**
 * @typedef {string} league
 * @typedef {string[]} team - array of team names
 * @typedef {string[]} rank - array of rank names
 * @typedef {string[]} division - array of division names
 * @typedef {string[]} conference - array of conference names
 * @returns { [league, team?, rank?, division?, conference?] }
 */

const

const foo = [1, 2, 3]
// ^ type number[]

const bar = /** @type {const} */ ([1, 2, 3])
// ^ type readonly [1, 2, 3]

Generics

/**
 * @template T
 * @param {T} input
 * @returns {T[]}
 */
function arrayify(input) {
    return [input]
}
/**
 * @template {Array} T
 * @param {T} array
 * @param {any} additional
 * @returns {T}
 */
function append(array, additional) {
	array.push(additional)
	return array
}

append(['a', 1, 'b'], 2) // works even if the array isn't homogenous
append('ab', 'c') // errors because 'ab' doesn't extend array

Define reusable types

/**
 * @typedef {Object} leagueParam
 * @property {string} league
 * @property {string[]} team
 * @property {string} rank
 * @property {string} division
 * @property {string} conference
 * @returns {leagueParam}
 */

Import types

/** @typedef {import('fruit/types').Apple} Apple */

/** @type {Apple} */
const redApple = new Apple('red')

/** @type {Apple} */
const greenApple = new Apple('green')
/** @import { Apple } from 'fruit/types' */

/** @type {Apple} */
const redApple = new Apple('red')

/** @type {Apple} */
const greenApple = new Apple('green')

Function overloads

/**
 * @overload
 * @param {string} param
 * @param {false} allowsMultiple
 * @returns {string}
 */

/**
 * @overload
 * @param {string} param
 * @param {true} allowsMultiple
 * @returns {string[]}
 */

/* returns a string if allowsMultiple is false, and an array of strings if allowsMultiple is true */
function getParamValue(param, allowsMultiple) { ... }

See also