Skip to main content
โšก Calmops

Static Methods and Properties in JavaScript

Static Methods and Properties in JavaScript

Introduction

Static methods and properties belong to the class itself rather than to instances of the class. They’re useful for creating utility functions, constants, and class-level state that doesn’t need to be replicated across instances. Understanding static members is essential for writing efficient, well-organized object-oriented JavaScript code.

In this article, you’ll learn how to create and use static methods and properties, explore practical patterns, and understand when to use them.

Understanding Static Members

Static Properties

Static properties are variables that belong to the class, not to instances.

// Basic static property
class MathUtils {
  static PI = 3.14159;
  static E = 2.71828;
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.E); // 2.71828

// Accessing from instance (not recommended)
const utils = new MathUtils();
console.log(utils.PI); // 3.14159 (inherited from class)

Static Methods

Static methods are functions that belong to the class, not to instances.

// Basic static method
class Calculator {
  static add(a, b) {
    return a + b;
  }
  
  static subtract(a, b) {
    return a - b;
  }
  
  static multiply(a, b) {
    return a * b;
  }
}

console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
console.log(Calculator.multiply(3, 7)); // 21

// Cannot call on instance
const calc = new Calculator();
// calc.add(5, 3); // TypeError: calc.add is not a function

Static vs Instance Members

Key Differences

// Instance members vs static members
class Counter {
  // Instance property
  count = 0;
  
  // Static property
  static totalInstances = 0;
  
  constructor() {
    Counter.totalInstances++;
  }
  
  // Instance method
  increment() {
    this.count++;
  }
  
  // Static method
  static getTotalInstances() {
    return Counter.totalInstances;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
const counter3 = new Counter();

console.log(counter1.count); // 0 (instance property)
console.log(Counter.totalInstances); // 3 (static property)
console.log(Counter.getTotalInstances()); // 3 (static method)

counter1.increment();
console.log(counter1.count); // 1
console.log(counter2.count); // 0 (separate instance)

Practical Static Patterns

Pattern 1: Utility Functions

// Static methods as utility functions
class StringUtils {
  static capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }
  
  static reverse(str) {
    return str.split('').reverse().join('');
  }
  
  static isPalindrome(str) {
    const cleaned = str.toLowerCase().replace(/\s/g, '');
    return cleaned === this.reverse(cleaned);
  }
  
  static truncate(str, length) {
    return str.length > length ? str.slice(0, length) + '...' : str;
  }
}

console.log(StringUtils.capitalize('hello')); // 'Hello'
console.log(StringUtils.reverse('hello')); // 'olleh'
console.log(StringUtils.isPalindrome('racecar')); // true
console.log(StringUtils.truncate('Hello World', 5)); // 'Hello...'

Pattern 2: Constants

// Static properties as constants
class Config {
  static API_URL = 'https://api.example.com';
  static API_VERSION = 'v1';
  static TIMEOUT = 5000;
  static MAX_RETRIES = 3;
  
  static get FULL_API_URL() {
    return `${this.API_URL}/${this.API_VERSION}`;
  }
}

console.log(Config.API_URL); // 'https://api.example.com'
console.log(Config.TIMEOUT); // 5000
console.log(Config.FULL_API_URL); // 'https://api.example.com/v1'

Pattern 3: Factory Methods

// Static methods as factory functions
class User {
  constructor(name, email, role) {
    this.name = name;
    this.email = email;
    this.role = role;
  }
  
  // Factory method for admin
  static createAdmin(name, email) {
    return new User(name, email, 'admin');
  }
  
  // Factory method for regular user
  static createUser(name, email) {
    return new User(name, email, 'user');
  }
  
  // Factory method for guest
  static createGuest() {
    return new User('Guest', '[email protected]', 'guest');
  }
}

const admin = User.createAdmin('Alice', '[email protected]');
const user = User.createUser('Bob', '[email protected]');
const guest = User.createGuest();

console.log(admin); // User { name: 'Alice', email: '[email protected]', role: 'admin' }
console.log(user); // User { name: 'Bob', email: '[email protected]', role: 'user' }
console.log(guest); // User { name: 'Guest', email: '[email protected]', role: 'guest' }

Pattern 4: Singleton Pattern

// Static method for singleton pattern
class Database {
  static instance = null;
  
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = null;
    Database.instance = this;
  }
  
  connect() {
    this.connection = 'Connected to database';
    return this.connection;
  }
  
  static getInstance() {
    if (!Database.instance) {
      Database.instance = new Database();
    }
    return Database.instance;
  }
}

const db1 = new Database();
const db2 = new Database();
const db3 = Database.getInstance();

console.log(db1 === db2); // true (same instance)
console.log(db2 === db3); // true (same instance)

Pattern 5: Counter/Registry

// Static properties for tracking
class Product {
  static productCount = 0;
  static products = [];
  
  constructor(name, price) {
    this.id = ++Product.productCount;
    this.name = name;
    this.price = price;
    Product.products.push(this);
  }
  
  static getProductCount() {
    return Product.productCount;
  }
  
  static getAllProducts() {
    return Product.products;
  }
  
  static getProductById(id) {
    return Product.products.find(p => p.id === id);
  }
  
  static getTotalValue() {
    return Product.products.reduce((sum, p) => sum + p.price, 0);
  }
}

const product1 = new Product('Laptop', 999.99);
const product2 = new Product('Mouse', 29.99);
const product3 = new Product('Keyboard', 79.99);

console.log(Product.getProductCount()); // 3
console.log(Product.getTotalValue()); // 1109.97
console.log(Product.getProductById(2)); // Product { id: 2, name: 'Mouse', price: 29.99 }

Real-World Examples

Example 1: Logger Class

// Logger with static methods
class Logger {
  static LOG_LEVEL = {
    DEBUG: 0,
    INFO: 1,
    WARN: 2,
    ERROR: 3
  };
  
  static currentLevel = Logger.LOG_LEVEL.INFO;
  
  static debug(message) {
    if (this.currentLevel <= this.LOG_LEVEL.DEBUG) {
      console.log(`[DEBUG] ${message}`);
    }
  }
  
  static info(message) {
    if (this.currentLevel <= this.LOG_LEVEL.INFO) {
      console.log(`[INFO] ${message}`);
    }
  }
  
  static warn(message) {
    if (this.currentLevel <= this.LOG_LEVEL.WARN) {
      console.warn(`[WARN] ${message}`);
    }
  }
  
  static error(message) {
    if (this.currentLevel <= this.LOG_LEVEL.ERROR) {
      console.error(`[ERROR] ${message}`);
    }
  }
  
  static setLevel(level) {
    this.currentLevel = level;
  }
}

Logger.info('Application started');
Logger.warn('This is a warning');
Logger.error('An error occurred');

Logger.setLevel(Logger.LOG_LEVEL.DEBUG);
Logger.debug('Debug message now visible');

Example 2: Validation Class

// Validation with static methods
class Validator {
  static EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  static PHONE_REGEX = /^\d{10}$/;
  static URL_REGEX = /^https?:\/\/.+/;
  
  static isEmail(email) {
    return this.EMAIL_REGEX.test(email);
  }
  
  static isPhone(phone) {
    return this.PHONE_REGEX.test(phone);
  }
  
  static isURL(url) {
    return this.URL_REGEX.test(url);
  }
  
  static isStrongPassword(password) {
    return password.length >= 8 &&
           /[A-Z]/.test(password) &&
           /[a-z]/.test(password) &&
           /\d/.test(password) &&
           /[!@#$%^&*]/.test(password);
  }
  
  static validateForm(data, rules) {
    const errors = {};
    
    for (const [field, rule] of Object.entries(rules)) {
      if (rule === 'email' && !this.isEmail(data[field])) {
        errors[field] = 'Invalid email';
      }
      if (rule === 'phone' && !this.isPhone(data[field])) {
        errors[field] = 'Invalid phone';
      }
      if (rule === 'url' && !this.isURL(data[field])) {
        errors[field] = 'Invalid URL';
      }
      if (rule === 'required' && !data[field]) {
        errors[field] = 'Required field';
      }
    }
    
    return Object.keys(errors).length === 0 ? null : errors;
  }
}

console.log(Validator.isEmail('[email protected]')); // true
console.log(Validator.isPhone('1234567890')); // true
console.log(Validator.isStrongPassword('Pass123!')); // true

const formData = {
  email: '[email protected]',
  phone: '123',
  website: 'https://example.com'
};

const rules = {
  email: 'email',
  phone: 'phone',
  website: 'url'
};

const errors = Validator.validateForm(formData, rules);
console.log(errors); // { phone: 'Invalid phone' }

Example 3: API Client

// API client with static configuration
class APIClient {
  static BASE_URL = 'https://api.example.com';
  static DEFAULT_TIMEOUT = 5000;
  static DEFAULT_HEADERS = {
    'Content-Type': 'application/json'
  };
  
  static async request(endpoint, options = {}) {
    const url = `${this.BASE_URL}${endpoint}`;
    const config = {
      headers: { ...this.DEFAULT_HEADERS, ...options.headers },
      timeout: options.timeout || this.DEFAULT_TIMEOUT,
      ...options
    };
    
    try {
      const response = await fetch(url, config);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
  
  static get(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }
  
  static post(endpoint, data, options) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }
  
  static put(endpoint, data, options) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }
  
  static delete(endpoint, options) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
  
  static setBaseURL(url) {
    this.BASE_URL = url;
  }
  
  static setDefaultTimeout(timeout) {
    this.DEFAULT_TIMEOUT = timeout;
  }
}

// Usage
const users = await APIClient.get('/users');
const newUser = await APIClient.post('/users', { name: 'John', email: '[email protected]' });
await APIClient.delete('/users/1');

Example 4: Event Emitter

// Event emitter with static methods
class EventEmitter {
  static events = {};
  
  static on(eventName, callback) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(callback);
  }
  
  static off(eventName, callback) {
    if (this.events[eventName]) {
      this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    }
  }
  
  static emit(eventName, data) {
    if (this.events[eventName]) {
      this.events[eventName].forEach(callback => callback(data));
    }
  }
  
  static once(eventName, callback) {
    const wrapper = (data) => {
      callback(data);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
  }
  
  static clear() {
    this.events = {};
  }
}

// Usage
EventEmitter.on('user:login', (user) => {
  console.log(`User ${user.name} logged in`);
});

EventEmitter.on('user:logout', (user) => {
  console.log(`User ${user.name} logged out`);
});

EventEmitter.emit('user:login', { name: 'Alice' });
EventEmitter.emit('user:logout', { name: 'Alice' });

Static Methods with Inheritance

// Static methods in inheritance
class Animal {
  static species = 'Unknown';
  
  static getSpecies() {
    return this.species;
  }
}

class Dog extends Animal {
  static species = 'Canis familiaris';
}

class Cat extends Animal {
  static species = 'Felis catus';
}

console.log(Animal.getSpecies()); // 'Unknown'
console.log(Dog.getSpecies()); // 'Canis familiaris'
console.log(Cat.getSpecies()); // 'Felis catus'

Common Mistakes to Avoid

Mistake 1: Confusing Static and Instance Members

// โŒ Wrong - Trying to access static from instance
class MyClass {
  static staticProp = 'static';
  instanceProp = 'instance';
}

const obj = new MyClass();
console.log(obj.staticProp); // 'static' (works but not recommended)
console.log(obj.instanceProp); // 'instance'

// โœ… Correct - Use class for static, instance for instance
console.log(MyClass.staticProp); // 'static'
console.log(obj.instanceProp); // 'instance'

Mistake 2: Using ’this’ Incorrectly in Static Methods

// โŒ Wrong - 'this' refers to class, not instance
class Counter {
  static count = 0;
  
  static increment() {
    this.count++; // Works but confusing
  }
}

// โœ… Correct - Be explicit
class Counter {
  static count = 0;
  
  static increment() {
    Counter.count++;
  }
}

Mistake 3: Modifying Static Properties from Instances

// โŒ Wrong - Modifying shared static property
class User {
  static users = [];
  
  constructor(name) {
    this.name = name;
    User.users.push(this); // Modifies shared array
  }
}

// โœ… Better - Use instance methods for clarity
class User {
  static users = [];
  
  constructor(name) {
    this.name = name;
  }
  
  static addUser(user) {
    this.users.push(user);
  }
}

Mistake 4: Forgetting Static is Not Inherited by Instances

// โŒ Wrong - Assuming static is available on instance
class MyClass {
  static staticMethod() {
    return 'static';
  }
}

const obj = new MyClass();
// obj.staticMethod(); // TypeError: obj.staticMethod is not a function

// โœ… Correct - Call on class
MyClass.staticMethod(); // 'static'

Summary

Static members provide class-level functionality:

  • Static properties store class-level data
  • Static methods provide utility functions
  • Use static for constants and configuration
  • Factory methods are common static patterns
  • Static members are not inherited by instances
  • Useful for singletons and registries
  • Keep static methods pure when possible
  • Document static members clearly

Next Steps

Continue your learning journey:

Comments