Classes

JavaScript Classes

ES6 Class Syntax

CSCI 4513

Week 3, Lecture 6

Today's Learning Objectives

Constructors vs Classes

Not Quite the Same

JavaScript doesn't have classes like Java or Ruby. ES6 introduced class syntax - a new way to write object constructors and prototypes that looks like classical OOP but uses the same prototypal inheritance under the hood.

Property Getters and Setters

Two Types of Properties

Data Properties

Regular properties we've used so far:

let obj = {
  name: "John",
  age: 30
};

Key Concept

Data properties store values directly. Access them with obj.name or obj["age"].

Accessor Properties

Functions that get/set values:

let obj = {
  get prop() { },
  set prop(val) { }
};

Key Concept

Accessor properties run code when accessed. Look like properties, behave like functions!

Getters in Action

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  }
};

alert(user.fullName);  // John Smith
// Looks like a property, works like a function!

What to Learn

  • get keyword: Defines a getter method
  • No parentheses: Access as user.fullName, not user.fullName()
  • Computed value: The getter combines name and surname dynamically
  • this keyword: References the object itself
πŸ’‘ Key Insight: We read user.fullName normally (not as a function), but the getter runs behind the scenes.

Adding a Setter

let user = {
  name: "John",
  surname: "Smith",

  get fullName() {
    return `${this.name} ${this.surname}`;
  },

  set fullName(value) {
    [this.name, this.surname] = value.split(" ");
  }
};

// Setter is executed
user.fullName = "Alice Cooper";

alert(user.name);     // Alice
alert(user.surname);  // Cooper

What to Learn

  • set keyword: Defines a setter that runs on assignment
  • value parameter: Receives whatever is assigned
  • Destructuring: [a, b] = str.split(" ") splits string into parts
  • Two-way binding: Get and set work together as one "virtual" property

Now we have a "virtual" property that's both readable and writable!

Smart Getters/Setters

Add Validation Logic

let user = {
  get name() {
    return this._name;
  },

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short, need at least 4 characters");
      return;
    }
    this._name = value;
  }
};

user.name = "Pete";
alert(user.name);  // Pete

user.name = "";    // Name is too short...

What to Learn

  • Validation: Setters can reject invalid data before storing
  • _underscore convention: _name signals "private" (by convention only)
  • Early return: Invalid values stop execution with return
  • Encapsulation: External code uses name, internal storage is _name
πŸ’‘ Convention: Properties starting with _ are internal and shouldn't be accessed directly.

Getters for Compatibility

// Old implementation
function User(name, age) {
  this.name = name;
  this.age = age;
}

// New implementation - switched to birthday
function User(name, birthday) {
  this.name = name;
  this.birthday = birthday;

  // age is now calculated, but old code still works!
  Object.defineProperty(this, "age", {
    get() {
      let todayYear = new Date().getFullYear();
      return todayYear - this.birthday.getFullYear();
    }
  });
}

let john = new User("John", new Date(1992, 6, 1));
alert(john.age);  // Works! Calculated on the fly

What to Learn

  • API stability: Changed internal storage without breaking external code
  • Object.defineProperty: Adds getters to existing constructors
  • Computed property: age is calculated from birthday, always current
  • Backwards compatible: Old code using john.age still works!

Class Basic Syntax

class MyClass {
  // class methods
  constructor() { ... }
  method1() { ... }
  method2() { ... }
  method3() { ... }
}

// Create new object with all methods
const instance = new MyClass();

What to Learn

  • class keyword: Declares a class (blueprint for objects)
  • constructor: Special method called when using new
  • Methods: Functions defined inside the class body
  • No commas: Unlike objects, methods aren't comma-separated

Note: No commas between class methods! (Unlike object literals)

Class Example

class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }
}

// Usage:
let user = new User("John");
user.sayHi();  // John

What to Learn

  • constructor(name): Receives arguments passed to new User(...)
  • this.name: Creates a property on the new object
  • sayHi(): Method all User instances can call
  • new keyword: Required to create instances

What happens when new User("John") is called:

  1. A new object is created
  2. The constructor runs with the argument
  3. It assigns this.name = "John"

What Is a Class, Really?

class User {
  constructor(name) { this.name = name; }
  sayHi() { alert(this.name); }
}

// Proof: User is a function!
alert(typeof User);  // function

// More precisely, it's the constructor method
alert(User === User.prototype.constructor);  // true

// Methods are in User.prototype
alert(User.prototype.sayHi);  // the code of sayHi

// Two methods in the prototype
alert(Object.getOwnPropertyNames(User.prototype));
// constructor, sayHi

What to Learn

  • Classes are functions: Under the hood, class creates a function
  • Prototype-based: Methods are stored on User.prototype
  • Same mechanism: Uses the same prototypal inheritance as constructor functions
  • Syntactic sugar: Cleaner syntax, same underlying behavior

Class vs Constructor Function

With Class

class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    alert(this.name);
  }
}

let user = new User("John");
user.sayHi();

Pure Functions

function User(name) {
  this.name = name;
}

User.prototype.sayHi =
  function() {
    alert(this.name);
  };

let user = new User("John");
user.sayHi();

Key Comparison

Both create the same result! Class syntax is cleaner and keeps methods together. Constructor functions require separate .prototype assignments. Same prototype inheritance underneath.

Not Just Syntactic Sugar

Important Differences

class User {
  constructor() {}
}

User();  // Error: Cannot be invoked without 'new'

What to Learn

Unlike constructor functions, classes enforce the new keyword. This prevents accidental bugs where forgetting new would pollute the global scope.

Class Expressions

// Anonymous class expression
let User = class {
  sayHi() {
    alert("Hello");
  }
};

// Named class expression
let User = class MyClass {
  sayHi() {
    alert(MyClass);  // Name visible only inside class
  }
};

new User().sayHi();  // works
alert(MyClass);      // Error: MyClass not visible outside

What to Learn

  • Anonymous: Class without internal name, assigned to variable
  • Named expression: Internal name for self-reference
  • Scope: Internal name only visible inside the class
  • Just like functions: Classes can be expressions too

Dynamic Classes "On-Demand"

function makeClass(phrase) {
  // Declare and return a class
  return class {
    sayHi() {
      alert(phrase);
    }
  };
}

// Create a new class dynamically
let User = makeClass("Hello");

new User().sayHi();  // Hello

What to Learn

  • Factory pattern: Function that creates and returns a class
  • Closure: The class "remembers" phrase from when it was created
  • Dynamic behavior: Different calls create different classes
  • Advanced technique: Useful for configurable class generation
πŸ’‘ Cool Feature: Classes can be created dynamically, just like functions!

Getters/Setters in Classes

class User {
  constructor(name) {
    // Invokes the setter
    this.name = name;
  }

  get name() {
    return this._name;
  }

  set name(value) {
    if (value.length < 4) {
      alert("Name is too short.");
      return;
    }
    this._name = value;
  }
}

let user = new User("John");
alert(user.name);  // John

user = new User("");  // Name is too short.

What to Learn

  • Constructor calls setter: this.name = name triggers the setter!
  • Validation in constructor: Invalid data rejected at creation time
  • Private backing field: _name stores the actual value
  • Consistent API: External code always uses validated name

Computed Method Names

class User {
  // Computed method name using brackets
  ['say' + 'Hi']() {
    alert("Hello");
  }
}

new User().sayHi();  // Hello

What to Learn

  • Bracket notation: [expression] evaluates to method name
  • Dynamic names: Method names can be computed at runtime
  • String concatenation: 'say' + 'Hi' becomes 'sayHi'
  • Advanced use: Variables, symbols, or any expression works

Just like object literals, classes support computed property names with [...]

Class Fields

Adding Properties Directly

class User {
  name = "John";  // Class field

  sayHi() {
    alert(`Hello, ${this.name}!`);
  }
}

new User().sayHi();  // Hello, John!

What to Learn

  • Class fields: Properties declared directly in class body
  • Default values: Set initial values without constructor
  • Per-instance: Each object gets its own copy
  • Modern syntax: ES2022 feature, now widely supported

⚠️ Important: Class fields are set on individual objects, NOT on User.prototype

Class Fields Location

class User {
  name = "John";
}

let user = new User();
alert(user.name);              // John
alert(User.prototype.name);    // undefined

// Field is on the instance, not the prototype!

What to Learn

  • Instance property: name exists on user object
  • Not on prototype: User.prototype.name is undefined
  • Key difference: Methods go on prototype, fields go on instances
  • Memory impact: Each instance has its own copy

Class fields can use complex expressions and function calls:

class User {
  name = prompt("Name, please?", "John");
}

Losing 'this' Problem

class Button {
  constructor(value) {
    this.value = value;
  }

  click() {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000);  // undefined
// 'this' is lost!

What to Learn

  • Context loss: Passing method as callback loses this
  • Why undefined: this becomes global/undefined in callback
  • Common gotcha: Event handlers and timers often trigger this
  • Same with: addEventListener, map, forEach

⚠️ Problem: When passed to setTimeout, the method loses its this context.

Solution: Arrow Function Fields

class Button {
  constructor(value) {
    this.value = value;
  }

  // Arrow function field - binds 'this'!
  click = () => {
    alert(this.value);
  }
}

let button = new Button("hello");

setTimeout(button.click, 1000);  // hello
// Works! 'this' is preserved

What to Learn

  • Arrow functions: Inherit this from surrounding scope
  • Class field syntax: click = () => {} creates bound method
  • Per-instance: Each object gets its own function (memory trade-off)
  • Safe callbacks: Always use arrow fields for event handlers
πŸ’‘ How it works: Arrow function field creates a separate function per object with this bound correctly.

Class Inheritance

Extending Classes with 'extends'

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    alert(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  speak() {
    alert(`${this.name} barks.`);
  }
}

let dog = new Dog("Rex");
dog.speak();  // Rex barks.

What to Learn

  • extends keyword: Dog inherits from Animal
  • Inherited constructor: Dog uses Animal's constructor automatically
  • Method override: Dog's speak() replaces Animal's
  • Property access: this.name works because it's inherited

The 'super' Keyword

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    alert(`${this.name} makes a sound.`);
  }
}

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

  speak() {
    super.speak();  // Call parent method
    alert(`${this.name} is a ${this.breed}.`);
  }
}

What to Learn

  • super(): Calls parent constructor - MUST be first in child constructor
  • super.method(): Calls parent version of a method
  • Extend, don't replace: Add behavior while keeping parent functionality
  • Required rule: Use this only AFTER calling super()

Private Class Fields

True Encapsulation with #

class BankAccount {
  #balance = 0;  // Private field

  deposit(amount) {
    this.#balance += amount;
  }

  getBalance() {
    return this.#balance;
  }
}

let account = new BankAccount();
account.deposit(100);
alert(account.getBalance());  // 100
alert(account.#balance);      // Error: Private field!

What to Learn

  • # prefix: Makes field truly private (enforced by JS engine)
  • Declaration required: Must declare #balance in class body
  • Access control: Only class methods can read/write
  • vs _underscore: # is enforced, _ is just convention
πŸ’‘ Note: Private fields must be declared in the class body and start with #

Private Methods

class User {
  #password;

  constructor(username, password) {
    this.username = username;
    this.#password = this.#hashPassword(password);
  }

  // Private method
  #hashPassword(password) {
    return password.split('').reverse().join('');
  }

  checkPassword(password) {
    return this.#hashPassword(password) === this.#password;
  }
}

let user = new User("john", "secret");
user.#hashPassword("test");  // Error: Private method!

What to Learn

  • # for methods: Same syntax makes methods private
  • Internal helper: #hashPassword is implementation detail
  • Public API: checkPassword is the safe interface
  • Security pattern: Hide sensitive logic from external access

Static Methods

Methods on the Class Itself

class User {
  static compare(a, b) {
    return a.name > b.name ? 1 : -1;
  }

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

let users = [
  new User("John"),
  new User("Alice")
];

users.sort(User.compare);  // Call static method
alert(users[0].name);      // Alice

What to Learn

  • static keyword: Method belongs to class, not instances
  • Called on class: User.compare(), not user.compare()
  • Utility functions: Operations that work with instances but don't need this
  • No this binding: this inside static methods refers to the class

Static methods are called on the class, not instances!

Static Properties

class Article {
  static publisher = "Daily News";
  static count = 0;

  constructor(title) {
    this.title = title;
    Article.count++;
  }

  static getPublisher() {
    return Article.publisher;
  }
}

let article1 = new Article("News 1");
let article2 = new Article("News 2");

alert(Article.count);        // 2
alert(Article.getPublisher()); // Daily News
alert(article1.publisher);   // undefined (not on instances!)

What to Learn

  • Shared data: One count shared by all instances
  • Class constants: publisher is configuration data
  • Access via class: Article.count, not article1.count
  • Counter pattern: Track how many instances created

Static vs Instance Members

Instance Members

  • Belong to object instances
  • Access via this
  • Different for each object
  • Use: object-specific data

Static Members

  • Belong to the class
  • Access via class name
  • Shared across all instances
  • Use: utility functions, counters

Class Syntax Summary

class MyClass {
  prop = value;              // Property (field)

  constructor(...) {         // Constructor
    // ...
  }

  method(...) {}             // Method

  get something(...) {}      // Getter
  set something(...) {}      // Setter

  #privateProp = value;      // Private field
  #privateMethod() {}        // Private method

  static staticProp = value; // Static property
  static staticMethod() {}   // Static method

  [Symbol.iterator]() {}     // Computed name
}

Quick Reference

  • Fields: Instance data (each object gets own copy)
  • Methods: Shared via prototype
  • Private (#): Hidden from outside
  • Static: Belongs to class itself
  • Getters/Setters: Computed properties

Review Questions

Click each card to reveal the answer!

❓ Question 1

What differences are there between object constructors and classes?

❓ Question 2

What are getters and setters?

❓ Question 3

How is inheritance used with classes?

❓ Question 4

What are private class fields and methods?

❓ Question 5

What are static properties and methods?

πŸ’‘ Practice: Try converting your Library project constructors to ES6 classes!

Today's Takeaways

You Can Now:

πŸ“… Next Class: ES6 Modules, npm, and Webpack