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
What is the difference between a constructor function and a regular function in JavaScript?
- A constructor function is intended to be used with the
newkeyword to create new objects. - When called with
new, a constructor function creates a new object, setsthisto that object, and returns it. - Regular functions are called without
newandthisdepends on how the function is called.
How does inheritance work in ES6 classes?
- Inheritance is achieved using the
extendskeyword. - A subclass extends a parent class and inherits its properties and methods.
- The
super()function is used within the subclass constructor to call the parent class constructor.
What are the advantages of using ES6 classes over constructor functions?
- Cleaner and more concise syntax.
- Easier to read and maintain.
- Simplifies inheritance with
extendsandsuper. - Supports static methods, getters, setters, and private fields.
Explain the role of `super()` in class inheritance.
super()calls the constructor of the parent class.- It is necessary to use
super()before accessingthisin a subclass constructor. - Allows access to parent class methods and properties.
Can you explain how to implement private variables in ES6 classes?
-
Use the
#prefix to declare private fields. -
Private fields are only accessible within the class body.
-
Example:
class Example { #privateField = 'secret' getSecret() { return this.#privateField } }
Let's continue exploring the next page. Take your time, and proceed when you're ready.