Objects & Object Methods in JavaScript

Objects are one of JavaScript's most powerful features, allowing you to store collections of related data and functionality. Understanding objects is essential for mastering JavaScript and building complex applications.

Object Fundamentals

Objects in JavaScript are collections of key-value pairs where each key is a string (or Symbol) and each value can be any data type, including other objects or functions.

// Object literal notation - the most common way to create objects
const person = {
firstName: 'John',
lastName: 'Doe',
age: 30,
isEmployed: true,
hobbies: ['reading', 'coding', 'hiking'],
address: {
street: '123 Main St',
city: 'Anytown',
country: 'USA'
}
};

// Empty object
const emptyObject = {};

// Using the Object constructor
const anotherObject = new Object();
anotherObject.property = 'value';

Accessing Object Properties

There are two primary ways to access object properties: dot notation and bracket notation.

// Dot notation
console.log(person.firstName); // 'John'
console.log(person.address.city); // 'Anytown'

// Bracket notation
console.log(person['lastName']); // 'Doe'
console.log(person['address']['country']); // 'USA'

// Bracket notation is useful when property names are dynamic
const propertyName = 'age';
console.log(person[propertyName]); // 30

// Bracket notation is required when property names have spaces or special characters
const product = {
'product name': 'Laptop',
'product-id': 'LP-2023'
};
console.log(product['product name']); // 'Laptop'
console.log(product['product-id']); // 'LP-2023'

Modifying Objects

Objects in JavaScript are mutable, meaning their properties can be changed even if the object itself is declared with const.

// Adding new properties
person.email = 'john.doe@example.com';
person['phone'] = '555-1234';

// Modifying existing properties
person.age = 31;
person['isEmployed'] = false;

// Deleting properties
delete person.hobbies;
console.log(person.hobbies); // undefined

// Checking if a property exists
console.log('firstName' in person); // true
console.log('salary' in person); // false

// Alternative way to check property existence
console.log(person.hasOwnProperty('lastName')); // true

Object Methods

When a function is stored as a property of an object, it's called a method. Methods allow objects to have behavior.

const calculator = {
value: 0,
add: function(num) {
this.value += num;
return this.value;
},
subtract: function(num) {
this.value -= num;
return this.value;
},
multiply: function(num) {
this.value *= num;
return this.value;
},
reset() { // Shorthand method syntax (ES6+)
this.value = 0;
return this.value;
}
};

calculator.add(5); // 5
calculator.multiply(2); // 10
calculator.subtract(3); // 7
calculator.reset(); // 0

The this Keyword

In object methods, this refers to the object the method belongs to. However, the value of this can change depending on how a function is called.

const user = {
name: 'Alice',
greet() {
console.log(`Hello, my name is ${this.name}`);
},
greetArrow: () => {
// 'this' in arrow functions is inherited from the surrounding scope
console.log(`Hello, my name is ${this.name}`); // 'this.name' will be undefined
}
};

user.greet(); // "Hello, my name is Alice"
user.greetArrow(); // "Hello, my name is undefined"

// The value of 'this' can change when a method is called in different contexts
const greetFunction = user.greet;
greetFunction(); // "Hello, my name is undefined" (in non-strict mode)

// Binding 'this' to ensure it refers to the correct object
const boundGreet = user.greet.bind(user);
boundGreet(); // "Hello, my name is Alice"

Object Destructuring

Object destructuring allows you to extract properties from objects and assign them to variables in a concise way.

const product = {
id: 'P123',
name: 'Smartphone',
price: 699.99,
specs: {
cpu: 'Octa-core',
ram: '8GB',
storage: '256GB'
}
};

// Basic destructuring
const { name, price } = product;
console.log(name); // 'Smartphone'
console.log(price); // 699.99

// Destructuring with new variable names
const { name: productName, price: productPrice } = product;
console.log(productName); // 'Smartphone'

// Nested destructuring
const { specs: { cpu, ram } } = product;
console.log(cpu); // 'Octa-core'

// Default values
const { discount = 0 } = product;
console.log(discount); // 0 (default value since it doesn't exist in product)

// Rest pattern
const { id, ...productDetails } = product;
console.log(productDetails); // { name: 'Smartphone', price: 699.99, specs: {...} }

Object.keys(), Object.values(), and Object.entries()

These methods provide ways to iterate over object properties and values.

const book = {
title: 'JavaScript: The Good Parts',
author: 'Douglas Crockford',
year: 2008,
pages: 176
};

// Object.keys() returns an array of property names
const keys = Object.keys(book);
console.log(keys); // ['title', 'author', 'year', 'pages']

// Object.values() returns an array of property values
const values = Object.values(book);
console.log(values); // ['JavaScript: The Good Parts', 'Douglas Crockford', 2008, 176]

// Object.entries() returns an array of [key, value] pairs
const entries = Object.entries(book);
console.log(entries);
// [
// ['title', 'JavaScript: The Good Parts'],
// ['author', 'Douglas Crockford'],
// ['year', 2008],
// ['pages', 176]
// ]

// Iterating over object properties
Object.keys(book).forEach(key => {
console.log(`${key}: ${book[key]}`);
});

Object Spread Operator

The spread operator (...) allows you to create copies of objects and merge objects together.

// Copying an object
const original = { a: 1, b: 2 };
const copy = { ...original };
console.log(copy); // { a: 1, b: 2 }

// Merging objects
const defaults = { theme: 'light', fontSize: 'medium' };
const userPreferences = { theme: 'dark' };
const settings = { ...defaults, ...userPreferences };
console.log(settings); // { theme: 'dark', fontSize: 'medium' }

// Adding properties while copying
const updatedCopy = { ...original, c: 3, d: 4 };
console.log(updatedCopy); // { a: 1, b: 2, c: 3, d: 4 }

// Note: spread creates a shallow copy, not a deep copy
const nested = { a: 1, b: { c: 2 } };
const shallowCopy = { ...nested };
shallowCopy.b.c = 3;
console.log(nested.b.c); // 3 (nested object is still referenced, not copied)

Object.assign()

Object.assign() is another way to copy and merge objects.

// Copying an object
const source = { a: 1, b: 2 };
const target = Object.assign({}, source);
console.log(target); // { a: 1, b: 2 }

// Merging multiple objects
const object1 = { a: 1 };
const object2 = { b: 2 };
const object3 = { c: 3 };
const merged = Object.assign({}, object1, object2, object3);
console.log(merged); // { a: 1, b: 2, c: 3 }

// Modifying the target object
const target2 = { x: 10 };
Object.assign(target2, { y: 20, z: 30 });
console.log(target2); // { x: 10, y: 20, z: 30 }

Object Property Descriptors

JavaScript allows you to define property attributes using property descriptors.

const user = {};

// Define a property with specific attributes
Object.defineProperty(user, 'name', {
value: 'John',
writable: true, // Can be changed
enumerable: true, // Shows up in loops
configurable: true // Can be deleted or modified
});

// Define a read-only property
Object.defineProperty(user, 'id', {
value: 'U12345',
writable: false, // Cannot be changed
enumerable: true,
configurable: false // Cannot be deleted or reconfigured
});

// Define a property with getter and setter
let _age = 30;
Object.defineProperty(user, 'age', {
get() {
return _age;
},
set(value) {
if (value < 0) {
throw new Error('Age cannot be negative');
}
_age = value;
},
enumerable: true,
configurable: true
});

console.log(user.age); // 30
user.age = 31; // Sets age to 31
// user.age = -5; // Throws an error

Prototypes and Inheritance

JavaScript uses prototype-based inheritance, where objects can inherit properties and methods from other objects.

// Constructor function
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

// Adding a method to the prototype
Person.prototype.getFullName = function() {
return `${this.firstName} ${this.lastName}`;
};

// Creating instances
const person1 = new Person('John', 'Doe');
const person2 = new Person('Jane', 'Smith');

console.log(person1.getFullName()); // 'John Doe'
console.log(person2.getFullName()); // 'Jane Smith'

// Inheritance with prototypes
function Employee(firstName, lastName, position) {
// Call the parent constructor
Person.call(this, firstName, lastName);
this.position = position;
}

// Set up inheritance
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;

// Add methods specific to Employee
Employee.prototype.getJobInfo = function() {
return `${this.getFullName()} - ${this.position}`;
};

const employee = new Employee('Alice', 'Johnson', 'Developer');
console.log(employee.getFullName()); // 'Alice Johnson'
console.log(employee.getJobInfo()); // 'Alice Johnson - Developer'

Classes (ES6+)

ES6 introduced class syntax, which provides a cleaner way to create objects and implement inheritance.

// Class declaration
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}

getInfo() {
return `${this.year} ${this.make} ${this.model}`;
}

static isVehicle(obj) {
return obj instanceof Vehicle;
}
}

// Creating an instance
const car = new Vehicle('Toyota', 'Corolla', 2022);
console.log(car.getInfo()); // '2022 Toyota Corolla'

// Inheritance with classes
class Car extends Vehicle {
constructor(make, model, year, doors) {
super(make, model, year); // Call parent constructor
this.doors = doors;
}

getInfo() {
return `${super.getInfo()}, ${this.doors} doors`;
}
}

const sedan = new Car('Honda', 'Civic', 2023, 4);
console.log(sedan.getInfo()); // '2023 Honda Civic, 4 doors'

// Getters and setters in classes
class BankAccount {
constructor(owner) {
this.owner = owner;
this._balance = 0; // Convention: underscore for "private" properties
}

// Getter
get balance() {
return `$${this._balance}`;
}

// Setter
set balance(value) {
if (isNaN(value) || value < 0) {
throw new Error('Invalid balance amount');
}
this._balance = value;
}

deposit(amount) {
this._balance += amount;
}
}

const account = new BankAccount('John');
account.deposit(100);
console.log(account.balance); // '$100'

Object Immutability

JavaScript provides several ways to make objects immutable or restrict modifications.

// Object.freeze() - prevents adding, removing, or changing properties
const frozenObj = { name: 'Cannot Change' };
Object.freeze(frozenObj);

// These operations will fail silently in non-strict mode
// or throw errors in strict mode
frozenObj.name = 'New Name'; // No effect
frozenObj.newProp = 'New'; // No effect
delete frozenObj.name; // No effect

console.log(frozenObj); // { name: 'Cannot Change' }
console.log(Object.isFrozen(frozenObj)); // true

// Object.seal() - prevents adding or removing properties, but allows changing existing ones
const sealedObj = { name: 'Can Change Value' };
Object.seal(sealedObj);

sealedObj.name = 'New Name'; // Works
sealedObj.newProp = 'New'; // No effect
delete sealedObj.name; // No effect

console.log(sealedObj); // { name: 'New Name' }
console.log(Object.isSealed(sealedObj)); // true

// Object.preventExtensions() - prevents adding properties, but allows changing or removing existing ones
const nonExtensible = { name: 'No New Props' };
Object.preventExtensions(nonExtensible);

nonExtensible.name = 'Changed'; // Works
nonExtensible.newProp = 'New'; // No effect
delete nonExtensible.name; // Works

console.log(nonExtensible); // { }
console.log(Object.isExtensible(nonExtensible)); // false

JSON and Objects

JavaScript Object Notation (JSON) is a text-based data format that's derived from JavaScript object syntax.

// Converting an object to JSON string
const data = {
name: 'Product',
price: 19.99,
inStock: true,
tags: ['electronics', 'gadget']
};

const jsonString = JSON.stringify(data);
console.log(jsonString);
// '{"name":"Product","price":19.99,"inStock":true,"tags":["electronics","gadget"]}'

// Pretty-printing JSON
const prettyJson = JSON.stringify(data, null, 2);
console.log(prettyJson);
// {
// "name": "Product",
// "price": 19.99,
// "inStock": true,
// "tags": [
// "electronics",
// "gadget"
// ]
// }

// Converting JSON string back to an object
const parsedData = JSON.parse(jsonString);
console.log(parsedData.name); // 'Product'

// Note: JSON.stringify() cannot handle functions, undefined, or symbols
const objWithFunction = {
name: 'Example',
method: function() { return true; },
undefinedProp: undefined,
symbolProp: Symbol('symbol')
};

console.log(JSON.stringify(objWithFunction));
// '{"name":"Example"}' - function, undefined, and symbol are omitted

Common Object Patterns

Here are some common patterns and best practices when working with objects.

// Factory pattern
function createPerson(firstName, lastName, age) {
return {
firstName,
lastName,
age,
getFullName() {
return `${this.firstName} ${this.lastName}`;
}
};
}

const person1 = createPerson('John', 'Doe', 30);
const person2 = createPerson('Jane', 'Smith', 25);

// Module pattern (encapsulation)
const counter = (function() {
// Private variable
let count = 0;

// Return an object with public methods
return {
increment() {
count++;
return count;
},
decrement() {
count--;
return count;
},
getValue() {
return count;
}
};
})();

console.log(counter.getValue()); // 0
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.getValue()); // 2

// Mixin pattern
const canEat = {
eat(food) {
console.log(`Eating ${food}`);
}
};

const canWalk = {
walk(distance) {
console.log(`Walking ${distance} meters`);
}
};

const canSwim = {
swim(distance) {
console.log(`Swimming ${distance} meters`);
}
};

// Create an object with multiple behaviors
const person = Object.assign({}, canEat, canWalk);
person.eat('apple'); // 'Eating apple'
person.walk(100); // 'Walking 100 meters'

const fish = Object.assign({}, canEat, canSwim);
fish.eat('worm'); // 'Eating worm'
fish.swim(50); // 'Swimming 50 meters'

Best Practices

Follow these best practices when working with objects in JavaScript:

  • Use object literals for simple objects and classes for complex objects with behavior
  • Use descriptive property and method names
  • Avoid adding properties directly to the prototype of built-in objects
  • Use getters and setters for properties that require validation or computed values
  • Consider using immutable patterns for objects that shouldn't change
  • Use object destructuring to extract properties cleanly
  • Prefer the spread operator or Object.assign() for shallow copying
  • Use deep cloning libraries for nested objects when needed
  • Use Object.freeze() for configuration objects that should never change

Common Pitfalls

Be aware of these common issues when working with objects:

  • Mutating objects unintentionally, especially when passing them as function arguments
  • Forgetting that object comparisons check references, not values
  • Not handling the context of this correctly, especially in callbacks
  • Creating memory leaks with circular references
  • Assuming JSON.stringify() can handle all object types
  • Not accounting for inherited properties when iterating
  • Using reserved keywords as property names without bracket notation

Practice Exercise: Building a Library System

Let's apply what we've learned by creating a simple library system using objects.

// Create a library system with books, users, and borrowing functionality

// Book class
class Book {
constructor(title, author, isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
this.isAvailable = true;
this.currentBorrower = null;
}

markAsBorrowed(user) {
if (!this.isAvailable) {
return `Sorry, ${this.title} is already borrowed.`;
}
this.isAvailable = false;
this.currentBorrower = user.id;
return `${this.title} has been borrowed by ${user.name}.`;
}

returnBook() {
if (this.isAvailable) {
return `${this.title} was not borrowed.`;
}
this.isAvailable = true;
this.currentBorrower = null;
return `${this.title} has been returned.`;
}

getInfo() {
return {
title: this.title,
author: this.author,
isbn: this.isbn,
status: this.isAvailable ? 'Available' : 'Borrowed'
};
}
}

// User class
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
this.borrowedBooks = [];
}

borrowBook(book) {
const result = book.markAsBorrowed(this);
if (book.currentBorrower === this.id) {
this.borrowedBooks.push(book.isbn);
}
return result;
}

returnBook(book) {
if (book.currentBorrower !== this.id) {
return `You did not borrow ${book.title}.`;
}
const result = book.returnBook();
this.borrowedBooks = this.borrowedBooks.filter(isbn => isbn !== book.isbn);
return result;
}

getBorrowedBooks(library) {
return this.borrowedBooks.map(isbn => {
const book = library.findBookByISBN(isbn);
return book ? book.getInfo() : null;
}).filter(book => book !== null);
}
}

// Library class
class Library {
constructor(name) {
this.name = name;
this.books = {};
this.users = {};
}

addBook(title, author, isbn) {
if (this.books[isbn]) {
return `A book with ISBN ${isbn} already exists.`;
}
const book = new Book(title, author, isbn);
this.books[isbn] = book;
return `${title} has been added to the library.`;
}

registerUser(id, name, email) {
if (this.users[id]) {
return `A user with ID ${id} already exists.`;
}
const user = new User(id, name, email);
this.users[id] = user;
return `${name} has been registered.`;
}

findBookByISBN(isbn) {
return this.books[isbn] || null;
}

findUserById(id) {
return this.users[id] || null;
}

getAvailableBooks() {
return Object.values(this.books)
.filter(book => book.isAvailable)
.map(book => book.getInfo());
}

getBorrowedBooks() {
return Object.values(this.books)
.filter(book => !book.isAvailable)
.map(book => ({
...book.getInfo(),
borrower: this.users[book.currentBorrower]?.name || 'Unknown'
}));
}
}

// Usage example
const cityLibrary = new Library('City Public Library');

// Add books
cityLibrary.addBook('JavaScript: The Good Parts', 'Douglas Crockford', '0596517742');
cityLibrary.addBook('Clean Code', 'Robert C. Martin', '9780132350884');
cityLibrary.addBook('Eloquent JavaScript', 'Marijn Haverbeke', '1593279507');

// Register users
cityLibrary.registerUser('U001', 'Alice Johnson', 'alice@example.com');
cityLibrary.registerUser('U002', 'Bob Smith', 'bob@example.com');

// Borrow and return books
const alice = cityLibrary.findUserById('U001');
const jsBook = cityLibrary.findBookByISBN('0596517742');

console.log(alice.borrowBook(jsBook)); // JavaScript: The Good Parts has been borrowed by Alice Johnson.
console.log(cityLibrary.getAvailableBooks()); // Shows remaining available books
console.log(cityLibrary.getBorrowedBooks()); // Shows JavaScript: The Good Parts as borrowed
console.log(alice.getBorrowedBooks(cityLibrary)); // Shows Alice's borrowed books

console.log(alice.returnBook(jsBook)); // JavaScript: The Good Parts has been returned.
console.log(cityLibrary.getAvailableBooks()); // Now includes JavaScript: The Good Parts again

Further Learning

To deepen your understanding of JavaScript objects, consider exploring:

  • Object-oriented design patterns in JavaScript
  • Advanced prototype manipulation
  • Proxy objects for metaprogramming
  • WeakMap and WeakSet for memory-efficient object references
  • Object.defineProperties() for defining multiple properties at once
  • Deep cloning techniques for complex nested objects