Object Creation and Class Patterns in JavaScript
Objects are a fundamental part of JavaScript, providing a way to store and manipulate data in a structured manner. JavaScript is a prototype-based language, but with the introduction of ES6 classes, it has become easier to implement object-oriented programming patterns. In this lesson, we will explore different methods of creating objects, understand constructor functions, and learn about ES6 classes and inheritance.
Object Creation in JavaScript
Object Literals
- Definition: The simplest way to create an object using curly braces
{}.
Example:
const person = {
name: 'Alice',
age: 30,
greet: function () {
console.log(`Hello, my name is ${this.name}.`)
},
}
person.greet() // Outputs: Hello, my name is Alice.
Explanation:
- The
personobject has propertiesname,age, and a methodgreet.
Using Object.create()
- Definition: Creates a new object with the specified prototype object and properties.
Example:
const animal = {
eat: function () {
console.log(`${this.name} is eating.`)
},
}
const dog = Object.create(animal)
dog.name = 'Buddy'
dog.eat() // Outputs: Buddy is eating.
Explanation:
dogis created withanimalas its prototype.
Constructor Functions
- Definition: Functions used to create multiple similar objects.
Syntax:
function ConstructorFunction(parameters) {
// Initialization code
}
Example:
function Car(make, model) {
this.make = make
this.model = model
this.getInfo = function () {
return `${this.make} ${this.model}`
}
}
const myCar = new Car('Toyota', 'Corolla')
console.log(myCar.getInfo()) // Outputs: Toyota Corolla
Explanation:
Caris a constructor function.- The
newkeyword creates a new object, setsthisto that object, and returns it.
ES6 Classes
- Definition: Syntactic sugar over JavaScript's existing prototype-based inheritance.
Syntax:
class ClassName {
constructor(parameters) {
// Initialization code
}
// Methods
}
Example:
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
greet() {
console.log(`Hi, I'm ${this.name} and I'm ${this.age} years old.`)
}
}
const bob = new Person('Bob', 25)
bob.greet() // Outputs: Hi, I'm Bob and I'm 25 years old.
Explanation:
Personis a class with a constructor and a methodgreet.
Constructor Functions in Detail
Understanding Constructor Functions
- Purpose: To create multiple objects with the same properties and methods.
- Convention: Constructor function names start with an uppercase letter.
How it works:
- When a function is invoked with
new:- A new object is created.
- The
thiskeyword is set to the new object. - The new object inherits from the constructor's
prototype. - The constructor function code runs, potentially modifying
this. - The new object is returned unless the constructor returns a non-primitive value.
Example:
function Circle(radius) {
this.radius = radius
}
Circle.prototype.area = function () {
return Math.PI * this.radius * this.radius
}
const circle = new Circle(5)
console.log(circle.area()) // Outputs: 78.53981633974483
Explanation:
- The
Circleconstructor initializes theradius. - The
areamethod is added toCircle.prototype, so all instances share the same method.
Best Practices with Constructor Functions
- Always use the
newkeyword when calling a constructor function. - Define shared methods on the constructor's prototype, not within the constructor.
- Use
thisto assign properties within the constructor.
Avoiding Errors:
- Forgetting
newcan lead to unexpected results, asthiswould refer to the global object in non-strict mode orundefinedin strict mode.
Example without new:
const circle = Circle(5) // Forgot 'new'
console.log(radius) // Outputs: 5 (in global scope)
Explanation:
- Without
new,this.radiusassignsradiusto the global scope.
Solution:
- Enforce the use of
new:
function Circle(radius) {
if (!(this instanceof Circle)) {
return new Circle(radius)
}
this.radius = radius
}
ES6 Classes and Inheritance
Defining Classes
- Classes are defined using the
classkeyword.
Syntax:
class ClassName {
constructor(parameters) {
// Initialization code
}
method1() {
// Method code
}
method2() {
// Method code
}
// Static methods
static staticMethod() {
// Static method code
}
}
Example:
class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
getArea() {
return this.width * this.height
}
}
const rect = new Rectangle(10, 5)
console.log(rect.getArea()) // Outputs: 50
Explanation:
Rectangleclass has a constructor and a methodgetArea.
Class Inheritance with extends
- Use
extendsto create a subclass that inherits from a parent class. - Use
super()to call the parent class's constructor.
Example:
class Animal {
constructor(name) {
this.name = name
}
speak() {
console.log(`${this.name} makes a noise.`)
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name) // Call parent constructor
this.breed = breed
}
speak() {
console.log(`${this.name} barks.`)
}
}
const dog = new Dog('Buddy', 'Golden Retriever')
dog.speak() // Outputs: Buddy barks.
Explanation:
DogextendsAnimal.- The
speakmethod inDogoverrides the one inAnimal.
Static Methods
- Defined using the
statickeyword. - Called on the class itself, not on instances.
Example:
class MathUtils {
static add(a, b) {
return a + b
}
}
console.log(MathUtils.add(2, 3)) // Outputs: 5
Explanation:
addis a static method and is called directly onMathUtils.
Getters and Setters
- Use
getandsetto define getters and setters.
Example:
class Person {
constructor(name) {
this._name = name
}
get name() {
return this._name.toUpperCase()
}
set name(newName) {
this._name = newName
}
}
const person = new Person('Charlie')
console.log(person.name) // Outputs: CHARLIE
person.name = 'Dave'
console.log(person.name) // Outputs: DAVE
Explanation:
- The getter
namereturns the name in uppercase. - The setter
nameallows updating the name.
Private Fields (ES2021)
- Use
#prefix to declare private fields.
Example:
class Counter {
#count = 0 // Private field
increment() {
this.#count++
console.log(`Count: ${this.#count}`)
}
}
const counter = new Counter()
counter.increment() // Outputs: Count: 1
// console.log(counter.#count); // SyntaxError: Private field '#count' must be declared in an enclosing class
Explanation:
#countis not accessible outside the class.
Combining Constructor Functions and Prototypes
- Before ES6 classes, inheritance was achieved using constructor functions and manipulating prototypes.
Example:
function Vehicle(make) {
this.make = make
}
Vehicle.prototype.drive = function () {
console.log(`${this.make} is driving.`)
}
function Car(make, model) {
Vehicle.call(this, make) // Inherit properties
this.model = model
}
Car.prototype = Object.create(Vehicle.prototype) // Inherit methods
Car.prototype.constructor = Car // Set constructor reference
Car.prototype.honk = function () {
console.log(`${this.make} ${this.model} says beep beep!`)
}
const myCar = new Car('Toyota', 'Corolla')
myCar.drive() // Outputs: Toyota is driving.
myCar.honk() // Outputs: Toyota Corolla says beep beep!
Explanation:
Carinherits fromVehicleboth properties and methods.
Advantages of ES6 Classes
- Cleaner Syntax: More readable and familiar to those from class-based OOP languages.
- Inheritance Simplification: Easier to implement inheritance with
extendsandsuper. - Static Methods: Conveniently define methods on the class.
- Getters and Setters: Easily define property accessors.
- Private Fields: With the introduction of private fields, data encapsulation is improved.
Best Practices
Use ES6 Classes for Object-Oriented Programming
- Reason: Cleaner syntax, better readability, and modern features.
Name Classes and Constructors with UpperCamelCase
- Convention: Helps differentiate constructors and classes from regular functions.
Example:
class UserAccount {
// ...
}
Use super() in Subclasses
- Reason: Ensure the parent class's constructor is called.
Define Methods on Prototypes
- For constructor functions, define shared methods on
ConstructorFunction.prototype.
Avoid Extending Built-in Objects
- Reason: Can lead to unexpected behavior and conflicts.
Be Mindful of this Binding
- In classes and constructor functions,
thisrefers to the instance. - Arrow functions do not have their own
this; be cautious when using them as methods.
Exercises
Exercise 1: Constructor Functions
Question:
Create a constructor function Book that takes title and author as parameters. Add a method getDetails to the prototype that returns a string with the book's details.
Answer:
function Book(title, author) {
this.title = title
this.author = author
}
Book.prototype.getDetails = function () {
return `${this.title} by ${this.author}`
}
const book = new Book('1984', 'George Orwell')
console.log(book.getDetails()) // Outputs: 1984 by George Orwell
Exercise 2: ES6 Class Inheritance
Question:
Create a class Shape with a method area() that returns 0. Then, create a subclass Square that extends Shape and overrides the area() method to calculate the area of the square.
Answer:
class Shape {
area() {
return 0
}
}
class Square extends Shape {
constructor(sideLength) {
super()
this.sideLength = sideLength
}
area() {
return this.sideLength * this.sideLength
}
}
const square = new Square(5)
console.log(square.area()) // Outputs: 25
Exercise 3: Using super()
Question:
Explain why the following code results in an error and fix it.
class Animal {
constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
this.type = 'Cat'
this.name = name
}
}
const kitty = new Cat('Whiskers')
console.log(kitty.name)
Answer:
Error:
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
Explanation:
- In a subclass constructor, you must call
super()before accessingthis.
Fixed Code:
class Animal {
constructor(name) {
this.name = name
}
}
class Cat extends Animal {
constructor(name) {
super(name)
this.type = 'Cat'
}
}
const kitty = new Cat('Whiskers')
console.log(kitty.name) // Outputs: Whiskers
Exercise 4: Private Fields
Question:
Implement a class BankAccount with a private field #balance. Provide methods to deposit, withdraw, and getBalance, ensuring that the balance cannot be accessed or modified directly from outside the class.
Answer:
class BankAccount {
#balance = 0
deposit(amount) {
if (amount > 0) {
this.#balance += amount
console.log(`Deposited: $${amount}`)
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount
console.log(`Withdrew: $${amount}`)
} else {
console.log('Insufficient funds')
}
}
getBalance() {
console.log(`Balance: $${this.#balance}`)
}
}
const account = new BankAccount()
account.deposit(100) // Outputs: Deposited: $100
account.withdraw(30) // Outputs: Withdrew: $30
account.getBalance() // Outputs: Balance: $70
// console.log(account.#balance); // SyntaxError
Exercise 5: Static Methods
Question:
Create a class Calculator with static methods add, subtract, multiply, and divide. These methods should perform the respective arithmetic operations.
Answer:
class Calculator {
static add(a, b) {
return a + b
}
static subtract(a, b) {
return a - b
}
static multiply(a, b) {
return a * b
}
static divide(a, b) {
if (b !== 0) {
return a / b
} else {
throw new Error('Division by zero')
}
}
}
console.log(Calculator.add(2, 3)) // Outputs: 5
console.log(Calculator.subtract(5, 2)) // Outputs: 3
console.log(Calculator.multiply(3, 4)) // Outputs: 12
console.log(Calculator.divide(10, 2)) // Outputs: 5
Understanding the various ways to create and work with objects in JavaScript, from constructor functions to modern ES6 classes, is essential for writing well-structured and maintainable code. By mastering these patterns and their nuances, you'll be better equipped to implement object-oriented programming concepts effectively and handle related technical interview questions with confidence.
Practice Problems
Inheritance in ES6 Classes
Constructor vs Regular Functions
ES6 Classes Advantages
Inheritance Role of Super
Implementing Private Variables in ES6 Classes
Let's continue exploring the next page. Take your time, and proceed when you're ready.