JavaScript BasicsFeatured

JavaScript Objects: The Definitive Guide

Master JavaScript objects from creation to advanced patterns. Learn properties, methods, prototypes, inheritance, and object-oriented programming.

By JavaScriptDoc Team
objectsjavascript basicspropertiesmethodsprototypesOOP

JavaScript Objects: The Definitive Guide

Objects are the foundation of JavaScript. Almost everything in JavaScript is an object or behaves like one. This comprehensive guide will teach you everything about objects, from basic creation to advanced patterns.

What is an Object?

An object is a collection of key-value pairs. The keys (also called properties) are strings, and the values can be any JavaScript data type.

// Simple object
const person = {
  name: 'John Doe',
  age: 30,
  isEmployed: true,
};

console.log(person.name); // 'John Doe'
console.log(person['age']); // 30

Creating Objects

Object Literal

The most common way to create objects:

const user = {
  firstName: 'Jane',
  lastName: 'Doe',
  age: 25,
  email: 'jane@example.com',

  // Method
  getFullName: function () {
    return `${this.firstName} ${this.lastName}`;
  },
};

// ES6 method shorthand
const calculator = {
  add(a, b) {
    return a + b;
  },
  subtract(a, b) {
    return a - b;
  },
};

Object Constructor

// Using new Object()
const obj1 = new Object();
obj1.name = 'John';
obj1.age = 30;

// Not recommended, use object literal instead

Object.create()

Creates a new object with specified prototype:

const personPrototype = {
  greet() {
    return `Hello, I'm ${this.name}`;
  },
};

const john = Object.create(personPrototype);
john.name = 'John';
console.log(john.greet()); // "Hello, I'm John"

// With property descriptors
const jane = Object.create(personPrototype, {
  name: {
    value: 'Jane',
    writable: true,
    enumerable: true,
    configurable: true,
  },
  age: {
    value: 25,
    writable: false,
  },
});

Constructor Functions

function Person(name, age) {
  this.name = name;
  this.age = age;

  this.greet = function () {
    return `Hello, I'm ${this.name}`;
  };
}

const person1 = new Person('John', 30);
const person2 = new Person('Jane', 25);

console.log(person1.greet()); // "Hello, I'm John"
console.log(person2.greet()); // "Hello, I'm Jane"

ES6 Classes

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

  greet() {
    return `Hello, I'm ${this.name}`;
  }

  static createGuest() {
    return new User('Guest', 'guest@example.com');
  }
}

const user = new User('John', 'john@example.com');
const guest = User.createGuest();

Accessing and Modifying Properties

Dot Notation vs Bracket Notation

const obj = {
  name: 'John',
  'favorite color': 'blue',
  123: 'numeric key',
};

// Dot notation
console.log(obj.name); // 'John'
obj.age = 30;

// Bracket notation (required for special cases)
console.log(obj['favorite color']); // 'blue'
console.log(obj[123]); // 'numeric key'

// Dynamic property access
const prop = 'name';
console.log(obj[prop]); // 'John'

// Computed property names
const key = 'dynamic';
const obj2 = {
  [key]: 'value',
  [`${key}2`]: 'value2',
};

Adding and Deleting Properties

const person = { name: 'John' };

// Adding properties
person.age = 30;
person['city'] = 'New York';

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

// Checking if property exists
console.log('age' in person); // true
console.log(person.hasOwnProperty('age')); // true

Object Methods

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

const person = {
  name: 'John',
  age: 30,
  city: 'New York',
};

// Get all keys
console.log(Object.keys(person));
// ['name', 'age', 'city']

// Get all values
console.log(Object.values(person));
// ['John', 30, 'New York']

// Get key-value pairs
console.log(Object.entries(person));
// [['name', 'John'], ['age', 30], ['city', 'New York']]

// Iterate over entries
for (const [key, value] of Object.entries(person)) {
  console.log(`${key}: ${value}`);
}

Object.assign()

// Shallow copy
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original);

// Merge objects
const target = { a: 1, b: 2 };
const source1 = { b: 3, c: 4 };
const source2 = { c: 5, d: 6 };

Object.assign(target, source1, source2);
console.log(target); // { a: 1, b: 3, c: 5, d: 6 }

// Clone with modifications
const user = { name: 'John', age: 30 };
const updatedUser = Object.assign({}, user, { age: 31 });

Object Spread Operator (ES6)

// Copying objects
const original = { a: 1, b: 2 };
const copy = { ...original };

// Merging objects
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged = { ...obj1, ...obj2 }; // { a: 1, b: 3, c: 4 }

// Adding/overriding properties
const user = { name: 'John', age: 30 };
const updatedUser = { ...user, age: 31, city: 'NY' };

// Nested spread
const nested = {
  a: 1,
  b: { c: 2, d: 3 },
};
const shallowCopy = { ...nested }; // b is still referenced

Property Descriptors

Getting and Setting Descriptors

const obj = { name: 'John' };

// Get property descriptor
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(descriptor);
// {
//   value: 'John',
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

// Define property with descriptor
Object.defineProperty(obj, 'age', {
  value: 30,
  writable: false, // Cannot be changed
  enumerable: true, // Shows up in for...in
  configurable: false, // Cannot be deleted or redefined
});

obj.age = 31; // Silently fails (strict mode throws error)
console.log(obj.age); // 30

Multiple Properties

const obj = {};

Object.defineProperties(obj, {
  name: {
    value: 'John',
    writable: true,
    enumerable: true,
  },
  age: {
    value: 30,
    writable: false,
    enumerable: true,
  },
  _id: {
    value: 123,
    enumerable: false, // Hidden from enumeration
  },
});

console.log(Object.keys(obj)); // ['name', 'age'] - _id is hidden

Getters and Setters

const person = {
  firstName: 'John',
  lastName: 'Doe',

  // Getter
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  // Setter
  set fullName(value) {
    const parts = value.split(' ');
    this.firstName = parts[0];
    this.lastName = parts[1];
  },
};

console.log(person.fullName); // 'John Doe'
person.fullName = 'Jane Smith';
console.log(person.firstName); // 'Jane'
console.log(person.lastName); // 'Smith'

// Using Object.defineProperty
const user = {
  _age: 0,
};

Object.defineProperty(user, 'age', {
  get() {
    return this._age;
  },
  set(value) {
    if (value < 0) {
      throw new Error('Age cannot be negative');
    }
    this._age = value;
  },
});

Object Freezing and Sealing

Object.freeze()

const obj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
  },
};

Object.freeze(obj);

// Cannot modify
obj.name = 'Jane'; // Silently fails
obj.newProp = 'value'; // Silently fails
delete obj.age; // Silently fails

// But nested objects are not frozen
obj.address.city = 'Boston'; // This works!

// Deep freeze function
function deepFreeze(obj) {
  Object.freeze(obj);
  Object.values(obj).forEach((value) => {
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value);
    }
  });
  return obj;
}

Object.seal()

const obj = { name: 'John', age: 30 };

Object.seal(obj);

// Can modify existing properties
obj.name = 'Jane'; // Works
obj.age = 31; // Works

// Cannot add or delete properties
obj.city = 'New York'; // Silently fails
delete obj.age; // Silently fails

// Check if sealed
console.log(Object.isSealed(obj)); // true

Object.preventExtensions()

const obj = { name: 'John' };

Object.preventExtensions(obj);

// Can modify existing
obj.name = 'Jane'; // Works

// Can delete
delete obj.name; // Works

// Cannot add new
obj.age = 30; // Silently fails

console.log(Object.isExtensible(obj)); // false

Prototypes and Inheritance

Understanding Prototypes

// Every object has a prototype
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

// Constructor function prototype
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function () {
  return `Hello, I'm ${this.name}`;
};

const john = new Person('John');
console.log(john.greet()); // "Hello, I'm John"

// Prototype chain
console.log(john.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true

Prototypal Inheritance

// Base constructor
function Animal(name) {
  this.name = name;
}

Animal.prototype.speak = function () {
  return `${this.name} makes a sound`;
};

// Derived constructor
function Dog(name, breed) {
  Animal.call(this, name); // Call parent constructor
  this.breed = breed;
}

// Set up inheritance
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// Add methods to derived prototype
Dog.prototype.bark = function () {
  return `${this.name} barks!`;
};

// Override parent method
Dog.prototype.speak = function () {
  return `${this.name} barks loudly!`;
};

const myDog = new Dog('Max', 'Golden Retriever');
console.log(myDog.speak()); // "Max barks loudly!"
console.log(myDog.bark()); // "Max barks!"

ES6 Class Inheritance

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

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

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

  bark() {
    return `${this.name} barks!`;
  }

  // Override parent method
  speak() {
    return super.speak() + ' - Woof!';
  }
}

const myDog = new Dog('Max', 'Golden Retriever');
console.log(myDog.speak()); // "Max makes a sound - Woof!"

Object Composition

// Composition over inheritance
const canSwim = {
  swim() {
    return `${this.name} is swimming`;
  },
};

const canFly = {
  fly() {
    return `${this.name} is flying`;
  },
};

const canWalk = {
  walk() {
    return `${this.name} is walking`;
  },
};

// Factory function with composition
function createDuck(name) {
  return Object.assign({ name }, canSwim, canFly, canWalk);
}

const duck = createDuck('Donald');
console.log(duck.swim()); // "Donald is swimming"
console.log(duck.fly()); // "Donald is flying"

Advanced Object Patterns

Factory Pattern

function createUser(name, email, role = 'user') {
  return {
    name,
    email,
    role,

    login() {
      console.log(`${this.name} logged in`);
    },

    logout() {
      console.log(`${this.name} logged out`);
    },

    hasPermission(action) {
      const permissions = {
        user: ['read'],
        admin: ['read', 'write', 'delete'],
      };

      return permissions[this.role].includes(action);
    },
  };
}

const admin = createUser('John', 'john@example.com', 'admin');
console.log(admin.hasPermission('delete')); // true

Module Pattern

const Calculator = (function () {
  // Private variables
  let result = 0;
  let history = [];

  // Private function
  function addToHistory(operation) {
    history.push(operation);
  }

  // Public API
  return {
    add(n) {
      result += n;
      addToHistory(`+${n}`);
      return this;
    },

    subtract(n) {
      result -= n;
      addToHistory(`-${n}`);
      return this;
    },

    getResult() {
      return result;
    },

    getHistory() {
      return [...history]; // Return copy
    },

    reset() {
      result = 0;
      history = [];
      return this;
    },
  };
})();

Calculator.add(5).subtract(2).add(10);
console.log(Calculator.getResult()); // 13
console.log(Calculator.getHistory()); // ['+5', '-2', '+10']

Singleton Pattern

const Database = (function () {
  let instance;

  function createInstance() {
    return {
      connection: null,

      connect(connectionString) {
        this.connection = connectionString;
        console.log(`Connected to ${connectionString}`);
      },

      disconnect() {
        this.connection = null;
        console.log('Disconnected');
      },
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();

const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance

Object Destructuring

// Basic destructuring
const user = {
  name: 'John',
  age: 30,
  email: 'john@example.com',
};

const { name, age } = user;
console.log(name, age); // 'John' 30

// Renaming variables
const { name: userName, email: userEmail } = user;

// Default values
const { phone = 'N/A' } = user;

// Nested destructuring
const person = {
  name: 'John',
  address: {
    city: 'New York',
    country: 'USA',
  },
};

const {
  address: { city },
} = person;
console.log(city); // 'New York'

// Rest properties
const { name: personName, ...rest } = person;
console.log(rest); // { address: { city: 'New York', country: 'USA' } }

// Function parameters
function greet({ name, age = 18 }) {
  return `Hello ${name}, you are ${age} years old`;
}

greet(user); // "Hello John, you are 30 years old"

Working with JSON

// Object to JSON
const obj = {
  name: 'John',
  age: 30,
  hobbies: ['reading', 'gaming'],
  address: {
    city: 'New York',
    country: 'USA',
  },
};

const json = JSON.stringify(obj);
console.log(json); // String representation

// Pretty printing
const prettyJson = JSON.stringify(obj, null, 2);

// Custom serialization
const customJson = JSON.stringify(obj, (key, value) => {
  if (key === 'age') return undefined; // Exclude age
  return value;
});

// JSON to Object
const parsed = JSON.parse(json);

// Parsing with reviver
const dateJson = '{"date":"2023-01-01T00:00:00.000Z"}';
const objWithDate = JSON.parse(dateJson, (key, value) => {
  if (key === 'date') return new Date(value);
  return value;
});

Performance Considerations

// Object property access performance
const obj = { a: 1, b: 2, c: 3 };

// Fastest: dot notation with known property
console.time('dot');
for (let i = 0; i < 1000000; i++) {
  const x = obj.a;
}
console.timeEnd('dot');

// Slower: bracket notation
console.time('bracket');
for (let i = 0; i < 1000000; i++) {
  const x = obj['a'];
}
console.timeEnd('bracket');

// Hidden classes optimization
// Good - consistent property order
function Point(x, y) {
  this.x = x;
  this.y = y;
}

// Bad - inconsistent property order
function BadPoint(x, y) {
  if (x > 0) {
    this.x = x;
    this.y = y;
  } else {
    this.y = y;
    this.x = x;
  }
}

Best Practices

  1. Use object literals for simple objects
  2. Prefer const for object declarations
  3. Use descriptive property names
  4. Consider using classes for complex objects
  5. Avoid modifying prototypes of built-in objects
  6. Use Object.freeze() for immutable objects
  7. Be careful with nested object references

Conclusion

JavaScript objects are incredibly flexible and powerful. Key concepts to remember:

  • Objects are collections of key-value pairs
  • Multiple ways to create objects (literal, constructor, class)
  • Properties can be accessed, added, and deleted dynamically
  • Prototypes enable inheritance and method sharing
  • Modern features like destructuring and spread operator simplify code
  • Understanding objects is crucial for JavaScript mastery

Master these concepts to write more effective object-oriented JavaScript!