Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State
Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State
What will I learn
- You will learn the difference between instance members and static members;
- how static methods and properties belong to the class itself, not to instances;
- common uses of static members: factory methods, utilities, and shared constants;
- how static blocks let you run setup code once when a class is defined;
- how static members are inherited through the class chain, and what
thismeans inside a static method.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-39 read, especially classes and inheritance.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
- Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
- Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
- Learn JS Series (#38) - The class Syntax: Sugar Over Prototypes, and What It Hides
- Learn JS Series (#39) - Class Inheritance: extends, super, and the Prototype Chain Again
- Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State (this post)
Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State
Last episode we chained classes together with extends and super, and I kept hammering one point until you were probably sick of it: none of that was new machinery, just the prototype chain wearing a nicer suit. Every member we have written so far, though, has lived at the level of the individual object -- this.name, speak(), describe(), all of them answering the question "what can THIS particular thing do?". Today we climb one level up and ask a different question: "what does the CLASS as a whole know and do, quite apart from any single instance it produces?" That is the territory of static members, and it is where you find the counters, the constants, the validators, and those neat little factory functions like Array.from that you have been using since episode 11 without ever stopping to notice they belong to the class, not to an array. So let's climb that step, because it is a small one and the view from up there is genuinely useful. ;-)
Solutions to Episode 39 Exercises
Exercise 1 - a Shape and a Circle:
class Shape {
constructor(name) { this.name = name; }
describe() { return `a shape called ${this.name}`; }
}
class Circle extends Shape {
constructor(name, radius) { super(name); this.radius = radius; }
area() { return Math.PI * this.radius ** 2; }
}
const c = new Circle("disc", 2);
console.log(c.describe(), c.area().toFixed(2)); // "a shape called disc" "12.57"
The insight: super(name) initializes the Shape part of the object before Circle adds radius on top. The subclass constructor accepts more data than the parent, forwards the shared bit up, and keeps the extra bit for itself -- exactly the separation of responsibilities we talked about last time.
Exercise 2 - extending a parent method:
class Circle2 extends Shape {
constructor(name, radius) { super(name); this.radius = radius; }
describe() {
return `${super.describe()} with area ${(Math.PI * this.radius ** 2).toFixed(2)}`;
}
}
console.log(new Circle2("disc", 2).describe()); // "a shape called disc with area 12.57"
The insight: super.describe() runs the parent's method with this STILL bound to the circle, so this.name resolves correctly, and then we append our own bit to the returned string. That is the clean "wrap the parent, add a little" pattern -- override without throwing away what the parent already does well.
Exercise 3 - a custom error:
class NotFoundError extends Error {
constructor(message, resource) {
super(message);
this.name = "NotFoundError";
this.resource = resource;
}
}
try {
throw new NotFoundError("missing", "user/42");
} catch (e) {
console.log(e.message, e.resource, e instanceof Error); // "missing" "user/42" true
}
The insight: super(message) was necessary to initialize the built-in Error (its message, its stack trace), so the subclass is a REAL Error that works everywhere errors are expected, while still carrying the extra resource you handed it. Skip the super call and you would not have a valid error object at all.
Now let's look at members that belong to the class as a whole, not to any single instance.
Instance versus static
Everything we have put in a class so far has been an instance member: it belongs to each object created from the class. this.name is per-instance -- every object gets its own copy. A method like speak() is called on an instance and operates on that instance's data. That is the normal case, and it is the right default for the overwhelming majority of your code.
But sometimes a piece of data or behaviour belongs to the class itself, not to any one object it produces. Think of a counter that tracks how many objects exist, or a constant that is the same for every instance, or a helper that does not need an instance at all to do its job. These are static members, marked with the static keyword, and you access them on the class, not on an instance:
class MathHelper {
static PI = 3.14159; // static property
static square(n) { return n * n; } // static method
}
console.log(MathHelper.PI); // 3.14159 - accessed on the CLASS
console.log(MathHelper.square(5)); // 25
const helper = new MathHelper();
// console.log(helper.square(5)); // TypeError: helper.square is not a function
MathHelper.square(5) works, but helper.square(5) throws, because square lives on the class object MathHelper, not on the instances it makes. This trips people up the first time, so let me be blunt about the mental model: a static member sits on the class, and an instance can NOT see it by climbing its prototype chain -- the class object is not on that chain. The distinction is worth memorising in one line: instance members answer "what can each object do?", static members answer "what can the class as a whole do?". A static method does not operate on a particular instance; it is associated with the class conceptually. If your method never touches this.something, that is a loud hint it might want to be static.
Static methods: factories and utilities
Two uses dominate static methods in real code. The first is factory methods: static methods that create and return instances, usually offering a more descriptive, intention-revealing alternative to the raw constructor. You have been using built-in examples for thirty episodes without noticing -- Array.from, Array.of, Object.keys, Promise.resolve. They are static because they operate at the class level (they make or transform things), not because they act on one pre-existing instance:
class User {
constructor(name, role) { this.name = name; this.role = role; }
static admin(name) { // factory: a clearer way to make an admin
return new User(name, "admin");
}
static guest() {
return new User("guest", "visitor");
}
}
const boss = User.admin("scipio"); // reads far better than new User("scipio", "admin")
console.log(boss); // User { name: 'scipio', role: 'admin' }
console.log(User.guest()); // User { name: 'guest', role: 'visitor' }
User.admin("scipio") is more readable and self-documenting than new User("scipio", "admin"), and it hides the "admin" magic string as an internal detail the caller no longer has to remember. Factory static methods are a clean way to offer several named, meaningful ways to construct instances -- especially handy when a class can be built from very different inputs (from JSON, from a database row, from defaults) and a single constructor signature would get muddled trying to handle them all. In stead of one constructor doing four jobs badly, you give it one honest job and add named factories around it.
The second common use is utility functions that are related to the class but do not need an instance to run, like MathHelper.square above, or a validator that checks whether some raw input could even become a valid instance:
class Email {
constructor(address) { this.address = address; }
static isValid(text) { // a utility that needs no instance
return typeof text === "string" && text.includes("@") && text.includes(".");
}
}
console.log(Email.isValid("[email protected]")); // true
console.log(Email.isValid("nope")); // false
Email.isValid inspects a plain string without ever needing an Email object, so it belongs on the class, not on instances. It would be silly to have to construct an Email just to ask "is this text even email-shaped?" -- the whole point is to answer that BEFORE you build one. A common pairing is a static validator plus a static factory: validate the raw input with one, then construct the instance with the other, and you have a small, honest little pipeline living entirely at the class level.
Static properties: shared state and constants
Static properties hold data at the class level, shared across all instances. They are perfect for constants that are the same for every object, and for class-wide counters or registries that track something about the class as a whole. The classic teaching example is counting how many instances have ever been created:
class Widget {
static count = 0; // ONE value, shared across the whole class
constructor(name) {
this.name = name;
Widget.count += 1; // bump the class-level counter, not this.count
}
}
new Widget("a");
new Widget("b");
new Widget("c");
console.log(Widget.count); // 3 - shared state, not per-instance
Widget.count is a single value owned by the class; every constructor call bumps it, so it tracks the total number of widgets ever made. Notice carefully that we write Widget.count (the class name), NOT this.count. If you wrote this.count += 1 you would be creating and incrementing a brand-new instance property on each object, all sitting at 1, and the shared class counter would never move -- a genuinely common bug for newcomers, and one that fails silently rather than loudly, which makes it worse. The rule of thumb: when the data is about the class, name the class. Static properties are the natural home for such shared, class-level state, whether that is a running counter, a registry of every instance, or a MAX_CONNECTIONS = 10 constant that no single instance has any business owning.
Static blocks: one-time setup
Sometimes initializing a static property needs more than a single expression. Maybe you need a loop, or some logic that references several static members and has to run in order, or a value that is derived from another static property you just built. A one-line field initializer cannot express that. Enter the static initialization block (static { ... }), a relatively recent addition to the language that runs exactly once, when the class is defined, and can set up static state with full statements:
class Registry {
static items = new Map();
static defaults;
static {
// runs ONCE when the class is defined, with full access to static members
for (const name of ["home", "about", "contact"]) {
Registry.items.set(name, `/${name}`);
}
Registry.defaults = [...Registry.items.keys()];
}
}
console.log(Registry.items.get("about")); // "/about"
console.log(Registry.defaults); // ["home", "about", "contact"]
The static { } block executes a single time as the class loads (not once per instance, not every time you touch the class -- ONCE, at definition time), letting you populate static state with real, multi-statement logic in stead of cramming everything into a one-line field. It is, in a very real sense, the class-level equivalent of a constructor: a constructor runs once per object to set that object up, and a static block runs once per class to set the class up. And because it runs after the static fields above it are initialized, you can safely read Registry.items inside the block to derive Registry.defaults from it, as we did here. Before static blocks existed, people faked this with an immediately-invoked function or a stray line of setup code sitting awkwardly below the class -- the static block is simply the honest, built-in place for that work.
Static members and inheritance
Static members are inherited too, but -- and this is the subtle part -- through the class chain, not the instance chain. When one class extends another, the subclass inherits the parent's static methods and properties, so you can call a parent's static method directly on the child, and it just works:
class Base {
static create() { return new this("from base"); } // 'this' is the class it's called on
constructor(label) { this.label = label; }
}
class Special extends Base {}
const s = Special.create(); // inherited static method
console.log(s instanceof Special); // true - 'this' inside create WAS Special
console.log(s instanceof Base); // true - Special is also a Base
console.log(s.label); // "from base"
Special.create() works because Special inherits create from Base -- remember from last episode that extends also links the Special constructor to the Base constructor (Object.getPrototypeOf(Special) === Base), and THAT second link is exactly the chain a static lookup climbs. Now here is the genuinely clever bit: inside a static method, this refers to the class it was called on, not the class it was written in. So when we call Special.create(), this is Special, and new this("from base") builds a Special, not a Base. Write the factory once in the parent, and every subclass gets a correct factory for its own type for free. This pattern -- return new this(...) in an inherited static method -- is a small piece of real elegance, and you will meet it in library code more than you might expect. Static inheritance follows the class-to-class chain, running parallel to the instance prototype chain but one level up, precisely as we proved with getPrototypeOf last time.
A quick look sideways: static in Python
Quite some of you came to this series from the Learn Python Series, so a short comparison sharpens what static really is. Python splits the idea in two, which actually makes the JavaScript version clearer by contrast. A Python @staticmethod is a plain function that happens to live in the class namespace and gets no special first argument -- that is JavaScript's utility-style static. A Python @classmethod receives the class as its first argument (cls), which is exactly what JavaScript's this gives you inside a static method:
class User:
count = 0 # class-level attribute (like static count)
def __init__(self, name, role):
self.name = name
self.role = role
User.count += 1 # bump the class attribute, like Widget.count
@staticmethod
def is_valid(text): # utility: no instance, no class needed
return isinstance(text, str) and "@" in text
@classmethod
def admin(cls, name): # factory: 'cls' is the class, like 'this'
return cls(name, "admin") # cls(...) builds the right subclass, like new this(...)
print(User.admin("scipio").role) # "admin"
print(User.count) # 1
print(User.is_valid("[email protected]")) # True
Line for line it maps onto what we just did: User.count is Widget.count, @staticmethod is_valid is Email.isValid, and @classmethod admin with its cls(...) call is JavaScript's static admin with new this(...). The interesting difference is that JavaScript does NOT force you to choose between @staticmethod and @classmethod up front -- every JS static method automatically has access to the calling class through this, so a single static keyword covers both of Python's cases. Whether you treat that as a convenience or a source of confusion is up to you; I find it convenient, as long as you remember that this-in-a-static-method means "the class", never "an instance".
When to use static (and when not)
Static members are the right tool when something is conceptually tied to the class rather than to any instance: utilities that need no instance data, factory methods that create instances, shared constants, and class-wide state like counters or registries. The test I keep in my head is dead simple: does this operation need a specific object's data via this.something? If yes, it is an instance method, full stop. If it works purely at the class level, or its whole job is to CREATE instances, it is a strong candidate for static.
Having said that, do not turn static into a dumping ground for every loosely related function you can think of. If you find yourself writing a class whose only purpose is to hold a pile of static methods that never touch the class at all -- no shared state, no factories, nothing -- then you have not written a class, you have written a namespace with extra steps. In JavaScript, plain module functions (we get to modules properly in Phase 8) are usually the clearer home for that. A class MathUtils { static add() {} static sub() {} } with no state is a code smell borrowed from languages that force everything into a class; JavaScript does not, so don't. But for genuine class-level behaviour and state -- counters, registries, constants, inheritable factories -- static members keep the related code exactly where it belongs, on the thing it describes.
Try it yourself
- Create a class
Temperaturewith an instance propertycelsiusand two static factory methodsfromFahrenheit(f)andfromKelvin(k)that each return a newTemperaturewith the correct celsius value. Create instances both ways and print theircelsius. - Add a static
countproperty to a class and increment it in the constructor. Create several instances and print the count. Explain in one sentence why you writeClassName.countand notthis.countinside the constructor. - Write a class with a
static { }block that builds a static lookup table (say, mapping day numbers 0-6 to day names) using a loop. Access an entry from the table, and explain in one sentence when the static block runs.
So what did we actually cover?
- Instance members belong to each object; static members (marked
static) belong to the class itself and are accessed on the class, not on instances -- an instance genuinely cannot see them. - Static methods are commonly used for factory methods (creating instances with descriptive, named alternatives to the constructor) and for utilities that need no instance, like validators.
- Static properties hold class-level shared state and constants -- an instance counter (
ClassName.count, NOTthis.count), a registry, aMAX_...constant. - Static initialization blocks (
static { }) run once when the class is defined, for setup that needs full statements, and can derive one static member from another. - Static members are inherited through the class-to-class chain; inside a static method
thisis the class it was called on, which makesnew this(...)an inheritable factory that builds the right subclass. - Use static for genuine class-level behaviour and state; do not use it as a dumping ground for unrelated functions -- for those, plain module functions are clearer.
Next episode we finally give classes real privacy: the # private fields that lock data away so no outside code can touch it, delivering the true encapsulation that closures gave us all the way back in Phase 2 -- but this time built right into the class.