JavaScript, the programming language
-
Created for making Web pages dynamic
-
Popular first-class programming language
- Full Stack JavaScript: use JavaScript everywhere
-
Quick to develop in
- Dynamically typed
- Just in Time compiled
- Object-Oriented and Functional
Effectively using JavaScript
-
Focus on how JavaScript works
- Understand what (not) to use when
-
We assume basic knowledge of JavaScript
- Syntax, functions, classes, error handling, …
-
WebAssembly enables alternative languages to be executed in Web pages
- Not in scope for this lecture
Objects represent things
-
Objects have properties
-
Object have methods
-
Object instantiation
Objects have properties and internal slots
-
Properties
- Accessible key-value mappings
-
Internal slots
- Inaccessible object properties defined by ECMAScript
- ECMAScripts denotes them with
[[SlotName]]
-
Examples:
[[Prototype]]
: Prototype of object
[[Extensible]]
: If properties can be added to object
[[PrivateFieldValues]]
: List of private class fields
Property descriptors encode attributes of properties
-
Object properties can be added or modified with
Object.defineProperty()
- Uses the ECMAScript internal method
[[DefineOwnProperty]]
- Can be read with
Object.getOwnPropertyDescriptor()
-
Two disjoint types of property descriptors
- Data descriptor: property with value
- Accessor descriptor: property with getter-setter functions
Data descriptors encode attributes of value-based properties
-
Value
- The value of this property
- Default:
value: undefined
-
Writability
- If this property can be changed through assignment
- Default:
writable: false
Object.defineProperty(dog, "name", { value: "Laika" });
Accessor descriptors encode attributes of getter-setter-based properties
-
Getter
- Function that returns the value of a property
- Default:
get: undefined
-
Setter
- Function that is invoked with an assigned property value
- Default:
set: undefined
Object.defineProperty(dog, "name", { get: () => "Laika" });
Common attributes for data and accessor descriptors
-
Configurability
- If attributes of this property can be changed
- Default:
configurable: false
-
Enumerability
- If this property is included when enumerating properties
- E.g.:
for…in
, Object.keys
- Default:
enumerable: false
Property descriptors allow fine-grained control
-
Normal assignment is less verbose
dog.name = "Samson";
- Property descriptors are defined implicitly
write
, configurable
, and enumerable
default to true
Prototype-based inheritance
-
Before ECMAScript had classes, inheritance was done using prototypes
- Classes are syntactical sugar on prototypes (since ES2015)
- Prototypes may be used directly, but are less convenient
-
[[Prototype]]
points to object parent
- Property access propagates through prototype chain.
- Manipulate with
Object.getPrototypeOf()
and Object.setPrototypeOf()
Creating prototypes manually
-
Prototypes are plain objects
Defining prototype chain manually
-
All Mammals are Animals
-
All Dogs are Mammals
Instantiating a prototype
-
Create a new Dog instance
-
Properties and methods from the whole prototype chain can be used
Avoid defining prototypes manually!
-
class
-based syntax is less verbose
-
Object.setPrototypeOf
is slow
- JavaScript engines do not optimize for it
Type Coercion implicitly converts values to the expected type
-
Initially, JavaScript had no exceptions support
- Function input: coerced to expected type (string → number)
- Function output: error value if something fails (
NaN
)
- Applies to older JavaScript features:
==
, +
-
Modern JavaScript features do not coerce
- They can throw exceptions instead
- E.g.,
in
, ===
Internal ToPrimitive()
function is used for many coercion algorithms
-
Accepts values of any type, and a hint
- Hint indicates the preferred type: string, number, or default
-
Features that prefer numbers
-
Features that prefer strings
-
Features that are neutral
ToPrimitive()
uses a fall-through approach
-
Five checks
- Return if a primitive
- Invoke optional
Symbol.toPrimitive
method
- Invoke
toString
or valueOf
if string hint
- Invoke
valueOf
or toString
otherwise
- Throw
TypeError
Example: Applying +
-
ToPrimitive()
is applied on both arguments of +
Example: Applying +
-
ToPrimitive("a")
- Already is a primitive, return as-is
Example: Applying +
-
ToPrimitive({ valueOf: () => 1 })
- Not a primitive
- No
Symbol.toPrimitive
method
- No string hint
- Invoke
valueOf
→ 1
Example: Applying +
-
+
operation can be applied
Avoid Type Coercion when possible
-
Use features that don't apply Type Coercion
-
Explicitly cast values to types
"25" + 1
Number("25") + 1
-
Explicitly check types in your own functions
Event-based runtime model
-
Designed for handling events
- User interactions
- Network requests
- …
-
JavaScript is single-threaded
- Events are handled in sequence via the event loop
- Callbacks for asynchronous operations (I/O, timers, …)
Callback functions for (a)synchronous completion
-
Two types
- Synchronous:
array.forEach(element => …)
- Asynchronous:
setTimeout(() => …, 10)
-
Asynchronous callbacks are invoked later
- Added as tasks on a queue
Engines process the macrotask queue
-
Macrotask queue contains tasks
- Registered
'click'
handler after user clicks
- Callback after scheduled
setTimeout
is due
- Executing a loaded
<script src="...">
- The next file chunk was read
- …
-
Engine processes tasks in the macrotask queue
Promises hold results of async operations
-
Datastructure with resolve and reject callbacks
async
/await
: syntactic sugar for promises
-
Without
async
/await
-
With
async
/await
Promises always handled asynchronously
-
Callbacks are added to a separate microtask queue
.then()
, .catch()
, .finally()
-
Enables prioritization of tasks
- Macrotasks are less urgent
- Microtasks are more urgent
Microtasks run before macrotasks
-
Microtask queue contains tasks
- Handling of Promises and
await
- Scheduled via
queueMicrotask
-
Engine processes tasks in the microtask queue
- Fully processed after each macrotask
- In FIFO order
Task scheduling impacts performance
-
Run-to-completion
- Tasks are processed completely before going to next
- Makes reasoning about program easier
- Long-running tasks block later tasks (avoid them!)
-
Rendering pipeline invoked in-between macrotasks
- Not in-between microtasks
-
Tasks block interaction
setTimeout
, setInterval
, requestAnimationFrame
- Web Workers run in separate thread (no DOM access)
Iterables and iterators
-
Objects can be made iterable
- Implementing the
[Symbol.iterator]
method
- Invoked by
for…of
, spread operator, …
-
[Symbol.iterator]
returns an iterator
next()
: returns { value }
or { done: true }
Generators provide syntactic sugar to create iterators
-
Generator functions produce generator objects
-
Generator objects are iterable
- They implement
[Symbol.iterator]
Async iterators are iterators returning promises
-
Objects can be made async iterable
- Implementing the
[Symbol.asyncIterator]
method
- Invoked by
for await…of
- Useful when iterating over async elements
- E.g.,
ReadableStream
contains chunks of HTTP body
-
[Symbol.asyncIterator]
returns an async iterator
next()
: returns promise to { value }
or { done: true }
Async generators provide syntactic sugar to create async iterators
-
Generator functions produce generator objects
-
Async generator objects are async iterable
- They implement
[Symbol.asyncIterator]
Building blocks for parallelism
-
Safe communication beyond a single event loop
- Web Workers, Worker Threads, WebAssembly, WebGL, …
-
Atomics
: Utilities for thread-safe atomic operations
- Add, exchange, lock, …
-
SharedArrayBuffer
: Binary data buffer
- Can be safely shared and sliced across threads
-
Transferable objects
- Transfer objects to other contexts, no sharing
Typed Arrays: Low-level binary arrays
-
Regular arrays can contain all value types
- Different than array types of languages like C and Java
-
Typed arrays can only contain specific values
Int8Array
: Signed bytes
UInt8Array
: Unsigned bytes
Int16Array
: Signed 16-bit short integers
- …
-
Useful when low-level access is required
- WebAssembly, WebGL, …
Typed Arrays require specific memory allocation
-
Size must be specified through constructor
- Number of elements or copy of another typed array
-
All Typed Arrays operate on an
ArrayBuffer
- Created internally or passed through constructor
ArrayBuffer
s are transferable
Run JavaScript Everywhere
-
2009: Environment for event-based Web servers
- Alternative to sequential servers that do not scale well to many concurrent connections
-
Major release every six months
- Odd versions: Limited maintenance
- Even versions: LTS 18 months + maintenance 12 months
- → Target even releases during development for stability
The Node.js principles
-
Small core
- Node.js API as small as possible, rest delegated to userland
-
Small reusable modules
- Small in scope and size; Don't Repeat Yourself (DRY)
-
Small surface area
- No access to internals, only a specific entry point
-
Simplicity and pragmatism
- Keep It Simple, Stupid (KISS)
Node.js is based on the V8 engine
-
V8: JavaScript execution engine designed for Chrome
- Highly performant execution of JavaScript
-
libuv: I/O engine of Node.js
- Abstracts I/O access across operating systems
-
Node.JS API: Core JavaScript library
- Dedicated APIs: Streams, child process, …
- Excludes browser-specific APIs: DOM,
window
, …
Reactor pattern for handling I/O
-
Callback-based & asynchronous
- Invoke operation on a resource using a handler
- Handler is invoked through event loop on completion
Node.js implements the event loop as multiple sequential phases
-
Macrotasks
- Poll: I/O-related callbacks
- Check:
setImmediate
- Close:
EventEmitter
"close"
events
- Timers:
setTimeout
and setInterval
- Pending: System events, such as errors in sockets
-
Microtasks
- Node.js:
process.nextTick
- V8: Promises and
queueMicrotask
Node.js is the most popular environment, but alternatives exist
-
Deno
- By creator of Node.js to avoid its flaws
- Importing modules through URLs
- More secure (opt-in flags)
- Runs TypeScript directly
-
Bun
- Runs on JavaScriptCore instead of V8
- Faster and more memory-friendly
- Not fully (yet) with Node.js
- Runs TypeScript directly
Splitting up code into modules
-
All code in a single file is unsustainable
- Code can be split into modules
-
Reducing complexity during development
- Focus on smaller parts of code
- Avoid conflicts when working in teams
-
Reusability
- Generic modules can be reused across projects
Node.js supports two types of modules
-
CommonJS (CJS)
-
ECMAScript Modules (ESM)
- Standardized later in ECMAScript
CommonJS: Node.js's original modules
-
Modules expose functionality using
module.exports
- Any kind of object: class, constants, objects, …
-
Modules can be imported using
require()
- Path to a file or directory
- Invokes Node.js's module resolution algorithm
Module resolution in CommonJS
const imported = require(mod);
-
mod
is the name of a core Node.js module → load
-
mod
starts with /
, ./
, or ../
- Directory: load
main
from package.json
or index.js
- File: load with fallback to
.js
, .json
, or .node
extensions
-
Look for directory in
./node_modules
matching mod
- Traverse parent directories until root is reached
Examples of module resolution in CommonJS
require('url')
- Node.js URL core module
require('./file')
./file.js
require('pad')
./node_modules/pad/index.js
ECMAScript modules: Native JavaScript modules
-
Modules expose functionality using
export
statements
- Any kind of object: class, constants, objects, …
-
Modules can be imported using
import
statements
- Path to a file or directory, or a URL
Differences between CJS and ESM
-
CJS loads synchronous, ESM asynchronous
- ESM can load remote files
-
ESM imports must be specified with extensions
- CJS allows file paths without extensions
-
ESM allows named and default exports per file
- CJS only allows one type per file
-
Browser support
- ESM can run in Node.js and browsers
- CJS must be bundled before it can run in browsers
CJS and ESM in practise
-
Dual packaging
- Distribution with both ESM (
*.mjs
) and CJS (*.cjs
) files
- Dual packaging hazard: loading separate of same package
- →
instanceof
checks can differ for exported classes
-
CJS/ESM hell
- Some libraries or tooling only support CJS or ESM
- As of Node 22: ESM loading with
require()
A package managers helps managing collections of JavaScript modules
-
Collection of tools
- to install, upgrade, configure, and remove packages
-
Packages are distributions of software and data
- Contains JavaScript modules and supporting files
A package manager for Node.js
-
npm registry
- Millions of public JavaScript packages
- Open: anyone can deploy and consume
-
Command-line tool to manage packages using
package.json
- Metadata: package name, author, …
- Dependencies on other packages
- Files to be shipped with the package
Node.js was created before npm
-
Developers had to manually add modules to
node_modules/
- npm was created independently to make this less tedious
- npm is now installed together with Node.js installations
-
The Node.js runtime is not aware of npm
- npm populates
node_modules/
- Node.js loads modules from
node_modules/
- → alternative package managers can be used!
npm is the most popular package managers, but alternatives exist
-
Yarn
- Initiated by Meta and Google (now independent)
- Solves performance issues with large projects
- Consistency and security
-
pnpm
- Hard-links packages to a global store
- Reduces disk space across installations
-
→ Both use npm's
package.json
-
Versions represent types of changes across releases
-
Meaning when version components increment
- Major: Incompatible API changes
- Minor: Added functionality (backwards-compatible)
- Patch: Bug fix (backwards-compatible)
-
Examples
- 1.0.0 → 1.0.1: A bug was fixed
- 1.0.0 → 2.0.0: A breaking change occurred
SemVer is useful for man and machine
-
Developers using on another library
- Detect type of change without having to go through changelog or commit history
-
Dependency resolution
- Deduplication of installed dependencies with compatible version ranges
Dependencies can be defined using SemVer ranges
-
"dependencies"
entry in packages.json
understand SemVer ranges
- Major range:
x
or *
- Minor range:
^1.0.4
or 1.x
or 1
→ default in npm
- Patch range:
~1.0.4
or 1.0.x
or 1.0
-
Example:
- "dependencies": {
"my_dep": "^1.0.0",
"another_dep": "~2.2.0"
}
Installing dependencies with a package manager
-
npm install
fetches dependencies of a package
- This includes all transitive dependencies
-
Dependency resolution algorithms
- Determine precise versions for version ranges
- Determine the install location of the dependency
A naive dependency resolution algorithm: nested node_modules/
-
Each dependency has its own
node_modules/
in which dependencies are installed
- Pro: Code isolation and no chance of semver conflicts
- Con: Deep trees and chance of repeated installations
-
Example:
dep1/
node_modules/
common_dep1/
common_dep2/
dep2/
node_modules/
common_dep1/
uncommon_dep1/
node_modules/
common_dep2/
Dependency resolution with deduping
-
Dependencies are deduplicated by hoisting them to highest possible
node_modules/
- Only if they are compatible with the SemVer range
-
Example:
-
dep1/
node_modules/
common_dep1/
common_dep2/
dep2/
node_modules/
common_dep1/
uncommon_dep1/
node_modules/
common_dep2/
→
dep1/
common_dep1/
common_dep2/
dep2/
uncommon_dep1/
Dependencies that are incompatible with the SemVer range are not hoisted
-
Example:
-
dep1/
node_modules/
common_dep1/
common_dep2/ # ^2.0.0
dep2/
node_modules/
common_dep1/
uncommon_dep1/
node_modules/
common_dep2/ # ^1.0.0
→
dep1/
common_dep1/
common_dep2/
dep2/
uncommon_dep1/
node_modules/
common_dep2/
The order in which dependencies are installed determines matters
-
Dependencies are hoisted in order of installation
- A different install order can lead to different tree structures
-
Re-ordering package tree can reduce size
npm dedupe
attempts to reduce duplication
-
package-lock.json
guarantees determinism
- Locks the tree and versions in semantic ranges
- Useful for collaborative projects
From callbacks to EventEmitters
-
Callbacks have limitations
- Responsible for a single type of event
- Can only notify a single observer
-
EventEmitters follow the Observer pattern
- Can handle multiple types of events
- Accepts registrations of multiple listeners per event
- API:
on(event, listener)
, once(event, listener)
emit(event, ...args)
removeListener(event, listener)
Conventions for asynchronous error handling in Node.js
Errors can not be handled in callbacks and EventEmitters using try-catch.
-
Optional first error argument in callbacks
readFile('foo.txt', 'utf8', (err, data) => {
if (err) {
// Handle error
} else {
// Handle data
}
});
-
'error'
events in EventEmitters
Streams are EventEmitters for processing asynchronous data streams
-
Alternative to processing data after buffering
- Memory efficient: Processing data in chunks
- Time efficient: Earlier of processing chunks
- Composable: Streams can be piped to each other
-
Different types of streams
Writable
: Stream to write to (e.g. file on disk)
Readable
: Stream to read from (e.g. file on disk)
Duplex
: Implements Writable
and Readable
(e.g. sockets)
Transform
: Duplex
that can modify data chunks (e.g. gzip)
Reading files using streams
-
Using the
fs
API from Node.js
import * as fs from 'node:fs';
const readStream = fs.createReadStream('foo.txt');
readStream.on('data', (chunk) => console.log(chunk));
readStream.on('error', (err) => console.error(err));
readStream.on('end', () => console.log('Done!'));
Node.js Streams preceded the Web Streams API (for browsers)
-
Web Streams API was inspired by Node.js Streams
- Improvements based on lessons learned
- Strictly different API
- Streams can be converted to each other
- Node.js support since 16.x
-
Many projects still work with Node.js Streams
- Even when used in browsers
-
Web Streams will replace Node.js Streams
Concepts in Node.js and Web Streams
-
Piping
- Chaining streams to each other
-
Backpressure
- Stream in pipeline notify others to pause sending chunks
- Avoids buildup of large buffers within pipelines
- High water mark: maximum desired buffer size
-
Teeing
- Splitting a stream into two identical copies
JS engines use just-in-time (JIT) compilation
-
A good basic understanding of them is important
- Can help optimizing your JavaScript code
-
We will look at the V8 engine
- Most modern engines work similarly
V8 has multiple types of compilers
-
Ignition is a JIT compiler
- Compiles JavaScript to bytecode
- Interpreted execution of bytecode
-
Turbofan is an optimizing compiler
- Runs in the background during execution
- Identifies and optimizes hot code
-
Preceded by:
- Sparkplug: No optimizations (short-lived)
- Maglev: Few optimizations (after Sparkplug)
- → Execution becomes gradually faster at runtime
Optimizing compilers learn shape and structure of running code
-
JavaScript numbers are 64-bit floating point values
- V8 identifies Small Integers (SMI)
- SMIs can be stored in 32 bits (e.g. array indices)
- Saves memory and improves performance
-
JavaScript can have dynamic structures
- V8 identifies common structures among objects
- Creates hidden classes in C structs
- Not for objects in dictionary mode: many properties, dynamically changing properties
Optimizers are speculative
-
Optimizers first try the fast path
- If it fails, fallback to unoptimized code
- Fail-path has penalty of being extra slow
-
Optimizers work well if your code is C-like
- Datatypes for specific fields are constant
- Objects have no dynamically changing properties
Development in JavaScript differs from other languages
-
Different possible target environments
- Web browsers
- Server-side runtime environments (Node.js, Deno, …)
-
Fast evolution of new language features
- But not all environments immediately support new features
-
Dynamic nature of JS is useful for fast prototyping
- Can cause difficulties for larger projects
Most JavaScript projects incorporate multiple development processes
-
Steps to be run by the developer or a Continuous Integration (CI) service:
- Linting: Static analysis of code
- Testing: Automated checking of code to validate behaviour
- Transpiling: Converting between languages or versions
- Bundling: Combining and preparing code for the browser
-
Package managers (e.g. npm) provide tools to simplify running these steps
- Running
"scripts"
in package.json
using npm run
npm run test
, npm run build
Linters analyze code to find and fix potential problems
-
Static analysis
- No execution of the code
- Analysis on the abstract syntax tree (AST) after parsing
-
Configurable through rules
- Each rule defines a potential problem or style preference
-
Some rules can be automatically fixed
- Transforming the AST and writing back to the original file
Examples of rules for ESLint
-
ESLint
- Popular open-source linter for JavaScript
-
no-unused-vars
- Reports variables that are defined but are not used
-
no-undefs
- Reports code that uses undeclared variables
Categories of linter rules
-
Code quality
- Unused variables
- Usage of undeclared variables
-
Formatting
- Tabs or spaces
- Keyword spacing
- Dedicated formatters exist, such as Prettier
Increasing confidence in code through automated testing
-
Fully automated
- Implemented as code or configuration
- Executed at some frequency or on every change/version
-
Different target levels of testing
- Unit: Small and isolated pieces of code
- Integration: Multiple components that are wired together
- System: The full system
Measuring tested domain as code coverage
-
Metric that indicates how much of the code is tested
- Full coverage does not imply correctness
-
Different types of coverage criteria:
- Function: considers executed functions
- Line: considers passed lines
- Branch: considers all possible control branches (e.g. if-else)
-
Projects can configure coverage thresholds
- All thresholds except for 100% or 0% are arbitrary
Jest is a popular testing framework for JavaScript
-
Zero configuration
- Requires minimal configuration
- Very configurable and extensible when needed
-
Supports many different types of projects
- Node.js, TypeScript, React, …
Transpilers make code compatible with different JavaScript versions
-
JavaScript-to-JavaScript transpilers
- Converts across different ECMAScript versions
- E.g. Babel
-
Something-to-JavaScript transpilers
- Converts another high-level language into JavaScript
- E.g. TypeScript
Babel makes modern JavaScript backwards-compatible
-
Older browsers or runtime environments may not support all modern ECMAScript features
-
Code transformation
- Modern syntax to old syntax (e.g. await → promises)
-
Polyfills
- Defines missing classes and APIs if they are missing
TypeScript is a superset of JavaScript with type definitions
-
JavaScript is not strictly typed
- Developers to consult documentation when using a library
- Can cause difficulties for larger projects
-
TypeScript adds typings syntax
- Add types for variables, class, parameters, …
-
Highly popular
tsc
is the TypeScript compiler
-
tsc
transpiles TypeScript into JavaScript
- Includes type-checking
- Output JavaScript on specific ECMAScript version (like Babel)
-
Highly configurable in strictness
- Require types for all function arguments?
- Require strict null checks?
-
Runtime environments natively support TypeScript
- Deno and Bun
- Node.js experimentally since version 22
Towards cross-platform JavaScript
-
Different APIs in browser and server
- Browser-specific: DOM, …
- Server runtime environment:
fs
in Node.js for file I/O, …
-
Different approaches to modules
- Runtime environments support CJS and ESM
- Modern browsers only support ESM
- Older browsers don't support modules
-
Differences in optimization concerns
- Split code across many (server) or few (browser) modules
Bundlers enable cross-platform JavaScript
-
Different APIs in browser and server
- Transpilers such as Babel, polyfills
-
Different approaches to modules
- Creating browser-ready bundles of projects
-
Differences in optimization concerns
- Reducing the number of files within bundles
- Optional optimization steps: minification
Bundlers work in two steps
-
Dependency resolution
- Create a dependency graph of all modules
-
Tree-shaking removes unused modules
- Does not work for dynamic imports
-
Packing
- Convert dependency graph into browser-executable bundle
- Other assets can be includes: HTML, CSS, …
Making smaller bundles for production
-
Minification
- Removing unnecessary whitespaces and comments
- Convention to use
.min.js
as extension
-
Uglification
- Minification + Shortening variable and function names
-
Source maps link minified to original code
- Modern browsers understand sourcemaps
- Convention to use
.min.js.map
as extension