JS: Functions & Classes

Part of the Scripting Language reference.

Function declarations

function greet(name) {
  return "Hello, " + name + "!"
}

log(greet("Alice"))   // => Hello, Alice!

Arrow functions

let double = (x) => x * 2
log(double(5))   // => 10

let add = (a, b) => a + b
log(add(3, 4))   // => 7

// Multi-line arrow with block body
let factorial = (n) => {
  if (n <= 1) return 1
  return n * factorial(n - 1)
}
log(factorial(5))   // => 120

Single-parameter arrows do not require parentheses:

let square = x => x * x
log(square(4))   // => 16

No-parameter arrows require empty parentheses:

let greet = () => "Hello!"
log(greet())   // => Hello!

Function expressions

Functions can be used as expressions - assigned to variables, passed as arguments, or used inline.

Anonymous function expression:

const double = function(x) { return x * 2 }
log(double(5))   // => 10

Named function expression - the name is only visible inside the function body (useful for recursion):

const f = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1)
}
log(f(5))           // => 120
log(typeof fact)    // => undefined  (name not visible outside)

Async function expression:

const fetchVal = async function(x) { return await Promise.resolve(x) }
log(await fetchVal(42))   // => 42

IIFE (Immediately Invoked Function Expression):

const result = (function(x) { return x + x })(10)
log(result)   // => 20

Function expressions can be used in any expression context:

const obj = { calc: function(x) { return x * 2 } }
log(obj.calc(5))   // => 10

Default parameters

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

log(greet())          // => Hello, world!
log(greet("Alice"))   // => Hello, Alice!

Rest parameters

Collects remaining arguments into an array.

function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0)
}

log(sum(1, 2, 3))       // => 6
log(sum(10, 20, 30, 40)) // => 100

Rest parameter must be the last parameter:

function first(head, ...tail) {
  log("head:", head)
  log("tail:", tail)
}

first(1, 2, 3, 4)
// => head: 1
// => tail: [2, 3, 4]

arguments Object

Every function receives an implicit arguments array containing all passed arguments.

function showArgs() {
  log(arguments)
}

showArgs(1, "two", true)   // => [1, two, true]

Note: In the engine, arrow functions also have their own arguments object (unlike standard JavaScript). See Differences from JavaScript.

Tagged template literals

A function can be called with a template literal as its argument:

function tag(strings, ...values) {
  let result = ""
  for (let i = 0; i < strings.length; i++) {
    result += strings[i]
    if (i < values.length) result += String(values[i]).toUpperCase()
  }
  return result
}

let name = "world"
log(tag`hello ${name}!`)   // => hello WORLD!

Closures

Functions capture variables from their enclosing scope.

function makeCounter() {
  let count = 0
  return () => {
    count++
    return count
  }
}

let counter = makeCounter()
log(counter())   // => 1
log(counter())   // => 2
log(counter())   // => 3

Recursion

function fib(n) {
  if (n <= 1) return n
  return fib(n - 1) + fib(n - 2)
}

log(fib(10))   // => 55

Higher-order functions

Functions can be passed as arguments and returned from other functions.

function apply(fn, value) {
  return fn(value)
}

log(apply(x => x * 3, 7))   // => 21

function multiplier(factor) {
  return (x) => x * factor
}

let triple = multiplier(3)
log(triple(5))   // => 15

Hoisting

Function declarations are fully hoisted - they can be called before their definition:

log(greet("Alice"))   // => Hello, Alice!

function greet(name) {
  return "Hello, " + name + "!"
}

var declarations are hoisted as undefined:

log(x)     // => undefined
var x = 5
log(x)     // => 5

let / const are NOT hoisted:

// log(y)   // ReferenceError: 'y' is not defined
let y = 10

Classes

Class declarations

class Dog {
  constructor(name) {
    this.name = name
  }
  bark() {
    return this.name + " says woof"
  }
}

const d = new Dog("Rex")
log(d.bark())   // => Rex says woof

A class without an explicit constructor gets a default empty one.

The new operator

new creates instances of user-defined classes, built-in types, and error constructors.

const p = new Point(3, 4)      // User-defined class
const d = new Date(2024, 0, 15) // Built-in type
const re = new RegExp("^hello", "i")
const s = new Set([1, 2, 3])
const err = new TypeError("bad type")  // Error constructor

The this keyword

this refers to the current instance inside constructors and methods.

Method chaining - methods that return this enable fluent APIs:

class Builder {
  constructor() { this.parts = [] }
  add(p) { this.parts.push(p); return this }
  build() { return this.parts.join("-") }
}

log(new Builder().add("a").add("b").add("c").build())   // => a-b-c

Inheritance (extends)

class Animal {
  speak() { return "generic sound" }
}
class Dog extends Animal {
  speak() { return "woof" }
}

log(new Dog().speak())   // => woof

Multi-level inheritance:

class A { base() { return "base" } }
class B extends A { whoAmI() { return "B" } }
class C extends B {}

const c = new C()
log(c.whoAmI() + "," + c.base())   // => B,base

super

super() calls the parent constructor. super.method() calls a parent method.

class Animal {
  constructor(name) { this.name = name }
}
class Dog extends Animal {
  constructor(name, breed) {
    super(name)
    this.breed = breed
  }
}

const d = new Dog("Rex", "Lab")
log(d.name + " is a " + d.breed)   // => Rex is a Lab

Static methods

Static methods belong to the class itself, not to instances.

class MathHelper {
  static add(a, b) { return a + b }
}

log(MathHelper.add(3, 4))   // => 7

Getters and setters

class Circle {
  constructor(radius) { this.radius = radius }
  get area() { return 3.14 * this.radius * this.radius }
}

log(new Circle(10).area)   // => 314
class Person {
  constructor(name) { this._name = name }
  get name() { return this._name }
  set name(val) { this._name = val.toUpperCase() }
}

const p = new Person("alice")
p.name = "bob"
log(p.name)   // => BOB

Class fields

class Counter {
  count = 0
  increment() { this.count++ }
}

const c = new Counter()
log(c.count)     // => 0
c.increment()
log(c.count)     // => 1

Static fields:

class Config {
  static defaultTimeout = 5000
}
log(Config.defaultTimeout)   // => 5000

Private fields (# prefix) - not accessible outside the class:

class Secret {
  #value = 42
  getValue() { return this.#value }
}

const s = new Secret()
log(s.getValue())   // => 42
log(s.value)         // => undefined  (private field not exposed)

Class expressions

const Foo = class {
  getValue() { return 42 }
}
log(new Foo().getValue())   // => 42

instanceof with Classes

class Animal {}
class Dog extends Animal {}

const d = new Dog()
log(d instanceof Dog)      // => true
log(d instanceof Animal)   // => true  (inheritance chain)

Async methods

class Fetcher {
  async getData() {
    return await Promise.resolve(42)
  }
}

const result = await new Fetcher().getData()
log(result)   // => 42

Next steps