Static properties and methods

Most methods you write belong to individual objects. Call member.greet() and this points at that one member. But sometimes a piece of behavior belongs to the class itself, not to any single instance made from it. For those cases, JavaScript gives you static methods and properties.

You mark them with the static keyword:

class Member {
  static staticMethod() {
    alert(this === Member);
  }
}

Member.staticMethod(); // true

Notice the call: Member.staticMethod(), not someMember.staticMethod(). The method hangs off the class name directly.

It’s just a property on the class

The static keyword is convenient syntax, nothing more. Writing the method inside the class body with static produces the same result as bolting it onto the class object afterward:

class Member { }

Member.staticMethod = function() {
  alert(this === Member);
};

Member.staticMethod(); // true

Both approaches put staticMethod on the Member function object itself. A class in JavaScript is a function, and functions are objects, so you can assign properties to them.

Why does this === Member come out true? Because of the plain old “object before the dot” rule for this. In Member.staticMethod(), the object to the left of the dot is Member, so inside the call this is Member.

Member (class)
static staticMethod()
called as Member.staticMethod()
Member.prototype
greet()
called as member.greet()
A static method lives on the class object; instance methods live on the prototype that instances delegate to.

What statics are actually for

A static method models an operation that concerns the class as a whole, or several instances at once, rather than the internal state of one object.

Take a class of Recipe objects. You want a way to compare two recipes by date. That comparison isn’t really the business of any single recipe; it’s a relationship between recipes. So it fits naturally as a static method:

class Recipe {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static compare(recipeA, recipeB) {
    return recipeA.date - recipeB.date;
  }
}

// usage
let recipes = [
  new Recipe("Pancakes", new Date(2021, 5, 12)),
  new Recipe("Omelette", new Date(2021, 2, 3)),
  new Recipe("Risotto", new Date(2021, 8, 20))
];

recipes.sort(Recipe.compare);

alert( recipes[0].title ); // Omelette

Recipe.compare sits “above” the recipes. It takes two of them and reports their ordering. Passing it straight to recipes.sort() works because sort expects exactly that shape of function: two arguments, a numeric result.

Factory methods

Another common use is the factory method: a static method that builds and returns an instance, often with some preset or computed data.

Suppose recipes can be created in more than one way:

  1. From explicit parameters (title, date, and so on) — that’s the constructor’s job.
  2. As a blank draft stamped with today’s date.
  3. Some other convenience shape down the line.

The second case reads well as a static method, Recipe.createDraft():

class Recipe {
  constructor(title, date) {
    this.title = title;
    this.date = date;
  }

  static createDraft() {
    // remember, here this === Recipe
    return new this("Untitled draft", new Date());
  }
}

let recipe = Recipe.createDraft();

alert( recipe.title ); // Untitled draft

The interesting line is new this(...). Since this inside the static method is the class Recipe, new this(...) means new Recipe(...). Writing new this instead of hard-coding new Recipe pays off with inheritance: a subclass that calls the inherited factory gets an instance of itself, not of the parent. More on that below.

Static methods also show up frequently in classes that talk to a database or an external store, where the operation targets records by identifier rather than an in-memory object:

// assuming Recipe is a class for managing stored recipes
// static method to remove a recipe by id:
Recipe.remove({id: 12345});
recipe
— [[Prototype]] →
Recipe.prototype
— [[Prototype]] →
Object.prototype
Recipe (class) — holds createDraft(), off to the side
recipe.createDraft walks the top row and never visits the class object.
Instance lookup never reaches the class object, so statics stay out of an instance's reach.

Static properties

Data can be static too. A static property looks like an ordinary class field, just with static in front:

class Recipe {
  static publisher = "Arjun Rao";
}

alert( Recipe.publisher ); // Arjun Rao

Read it off the class: Recipe.publisher. As with methods, the declaration is equivalent to assigning the property to the class object directly:

Recipe.publisher = "Arjun Rao";

Static properties are handy for class-level data that every instance shares or that describes the class itself: a default configuration value, a running count of created instances, a registry, a version string.

Inheritance of statics

Statics are inherited, just like regular methods. A subclass can reach the parent’s static members through its own name.

Here Bird defines a static property habitat and a static method compare. Sparrow extends Bird and gets both, accessible as Sparrow.habitat and Sparrow.compare:

class Bird {
  static habitat = "Sky";

  constructor(name, speed) {
    this.speed = speed;
    this.name = name;
  }

  fly(speed = 0) {
    this.speed += speed;
    alert(`${this.name} flies at speed ${this.speed}.`);
  }

  static compare(birdA, birdB) {
    return birdA.speed - birdB.speed;
  }

}

// Inherit from Bird
class Sparrow extends Bird {
  chirp() {
    alert(`${this.name} chirps!`);
  }
}

let sparrows = [
  new Sparrow("Speckled Sparrow", 10),
  new Sparrow("Brown Sparrow", 5)
];

sparrows.sort(Sparrow.compare);

sparrows[0].fly(); // Brown Sparrow flies at speed 5.

alert(Sparrow.habitat); // Sky

Sparrow never declares compare or habitat, yet Sparrow.compare and Sparrow.habitat resolve. When you call Sparrow.compare, the engine can’t find it on Sparrow, so it follows the chain up to Bird.compare and runs that.

How the lookup works: two prototype chains

extends wires up two separate [[Prototype]] links, and that’s the key to statics being inherited.

Sparrow (class)
— [[Prototype]] →
Bird (class)
statics: habitat, compare
Sparrow.prototype
— [[Prototype]] →
Bird.prototype
methods: fly
Top row carries static members. Bottom row carries instance methods.
extends creates two parallel prototype links: one between the class objects (statics) and one between the prototypes (instance methods).

Spelled out, class Sparrow extends Bird sets up:

  1. The Sparrow function prototypally inherits from the Bird function — this handles static members.
  2. Sparrow.prototype prototypally inherits from Bird.prototype — this handles regular instance methods.

Because both links exist, inheritance covers static and instance sides at the same time. You can confirm each link directly:

class Bird {}
class Sparrow extends Bird {}

// the class-object chain — for statics
alert(Sparrow.__proto__ === Bird); // true

// the prototype chain — for instance methods
alert(Sparrow.prototype.__proto__ === Bird.prototype); // true

Summary

Static methods hold behavior that belongs to a class “as a whole” rather than to any one instance. Typical cases are a comparison like Recipe.compare(a, b) and a factory like Recipe.createDraft(). You mark them with static in the class body.

Static properties store class-level data that isn’t tied to an instance — defaults, counters, registries, and the like.

The syntax:

class MyClass {
  static property = ...;

  static method() {
    ...
  }
}

Under the hood, a static declaration is the same as assigning to the class object itself:

MyClass.property = ...
MyClass.method = ...

Statics are inherited. For class B extends A, the prototype of B points to A (B.[[Prototype]] === A), so a static member not found on B is looked up on A. That second prototype link — between the class objects, alongside the usual one between the prototypes — is what makes static inheritance work.