JavaScript Objects: Properties, Methods, and Prototypes

Objects are JavaScript's fundamental data structure. Unlike primitives, objects store collections of related data and functionality as key-value pairs. Nearly everything in JavaScript is an object or...

Key Insights

  • JavaScript objects are dynamic collections of properties where every property has hidden attributes (writable, enumerable, configurable) that control its behavior beyond just storing values
  • The prototype chain is JavaScript’s inheritance mechanism—when you access a property, JavaScript walks up the chain until it finds the property or reaches null
  • Modern JavaScript provides powerful object utilities like Object.assign(), spread operators, and destructuring that make object manipulation cleaner and more predictable than manual property copying

Object Fundamentals

Objects are JavaScript’s fundamental data structure. Unlike primitives, objects store collections of related data and functionality as key-value pairs. Nearly everything in JavaScript is an object or behaves like one—arrays, functions, dates, and regular expressions all inherit from Object.

Creating objects is straightforward with literal notation:

const user = {
  name: 'Sarah Chen',
  email: 'sarah@example.com',
  age: 28
};

You can also use the constructor syntax, though it’s rarely necessary:

const user = new Object();
user.name = 'Sarah Chen';
user.email = 'sarah@example.com';

Property access works with dot notation or bracket notation. Use brackets when property names contain special characters, spaces, or when accessing properties dynamically:

console.log(user.name); // 'Sarah Chen'
console.log(user['email']); // 'sarah@example.com'

const propertyName = 'age';
console.log(user[propertyName]); // 28

Properties: Adding, Modifying, and Deleting

JavaScript objects are mutable. You can add, modify, or delete properties at any time:

const product = { name: 'Laptop', price: 999 };

// Add new property
product.stock = 15;

// Modify existing property
product.price = 899;

// Delete property
delete product.stock;

But properties have hidden attributes that control their behavior. Every property has three attributes: writable (can the value be changed?), enumerable (does it appear in for…in loops?), and configurable (can the property be deleted or its attributes changed?).

Use Object.defineProperty() for precise control:

const config = {};

Object.defineProperty(config, 'apiKey', {
  value: 'abc123xyz',
  writable: false,    // Cannot be changed
  enumerable: false,  // Won't show in Object.keys()
  configurable: false // Cannot be deleted
});

config.apiKey = 'newKey'; // Silently fails in non-strict mode
console.log(config.apiKey); // 'abc123xyz'

Check property existence carefully. The in operator checks the entire prototype chain, while hasOwnProperty() checks only the object itself:

const obj = { name: 'Test' };

console.log('name' in obj); // true
console.log('toString' in obj); // true (inherited from Object.prototype)

console.log(obj.hasOwnProperty('name')); // true
console.log(obj.hasOwnProperty('toString')); // false

Methods: Functions as Object Properties

Methods are simply functions stored as object properties. They encapsulate behavior with related data:

const calculator = {
  value: 0,
  add: function(n) {
    this.value += n;
    return this;
  },
  subtract: function(n) {
    this.value -= n;
    return this;
  }
};

calculator.add(10).subtract(3);
console.log(calculator.value); // 7

ES6 introduced method shorthand syntax, which is cleaner and should be your default:

const calculator = {
  value: 0,
  add(n) {
    this.value += n;
    return this;
  },
  subtract(n) {
    this.value -= n;
    return this;
  }
};

The this keyword refers to the object the method was called on, but context matters. Arrow functions don’t have their own this—they inherit it from the enclosing scope:

const timer = {
  seconds: 0,
  start() {
    // Regular function: 'this' refers to timer
    setInterval(function() {
      this.seconds++; // Wrong! 'this' is window/undefined
    }, 1000);
  },
  startCorrectly() {
    // Arrow function: 'this' inherited from startCorrectly
    setInterval(() => {
      this.seconds++; // Correct!
    }, 1000);
  }
};

Getters and setters provide computed properties and validation:

const person = {
  firstName: 'John',
  lastName: 'Doe',
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  
  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'

Prototypes and Inheritance

JavaScript uses prototypal inheritance. Every object has an internal link to another object called its prototype. When you access a property, JavaScript first checks the object itself, then walks up the prototype chain until it finds the property or reaches null.

Access an object’s prototype with Object.getPrototypeOf():

const obj = {};
const proto = Object.getPrototypeOf(obj);
console.log(proto === Object.prototype); // true

Create objects with a specific prototype using Object.create():

const animal = {
  eat() {
    console.log(`${this.name} is eating`);
  }
};

const dog = Object.create(animal);
dog.name = 'Rex';
dog.bark = function() {
  console.log('Woof!');
};

dog.eat(); // 'Rex is eating' (inherited from animal)
dog.bark(); // 'Woof!' (own property)

Constructor functions establish prototype relationships automatically:

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

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

const user1 = new User('Alice', 'alice@example.com');
const user2 = new User('Bob', 'bob@example.com');

console.log(user1.greet()); // 'Hello, I'm Alice'
console.log(user1.greet === user2.greet); // true (shared method)

The prototype chain enables efficient memory usage—methods are shared across all instances rather than duplicated.

Modern Object Utilities

JavaScript provides powerful utilities for object manipulation. Object.keys(), Object.values(), and Object.entries() extract data in different formats:

const user = { name: 'Alice', age: 30, role: 'admin' };

console.log(Object.keys(user)); // ['name', 'age', 'role']
console.log(Object.values(user)); // ['Alice', 30, 'admin']
console.log(Object.entries(user)); // [['name', 'Alice'], ['age', 30], ['role', 'admin']]

// Useful for iteration
Object.entries(user).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});

Object.assign() performs shallow copying and merging:

const defaults = { theme: 'light', notifications: true };
const userPrefs = { theme: 'dark' };

const settings = Object.assign({}, defaults, userPrefs);
console.log(settings); // { theme: 'dark', notifications: true }

Control object mutability with Object.freeze() and Object.seal():

const frozen = Object.freeze({ value: 42 });
frozen.value = 100; // Silently fails
frozen.newProp = 'test'; // Silently fails

const sealed = Object.seal({ value: 42 });
sealed.value = 100; // Works (can modify existing properties)
sealed.newProp = 'test'; // Fails (cannot add new properties)

The spread operator provides clean syntax for copying and merging:

const original = { a: 1, b: 2 };
const copy = { ...original };
const merged = { ...original, b: 3, c: 4 };
console.log(merged); // { a: 1, b: 3, c: 4 }

Practical Patterns and Best Practices

Modern JavaScript offers multiple approaches to object creation. Factory functions return new objects without using new:

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

const user = createUser('Alice', 'alice@example.com');

ES6 classes provide familiar syntax but are syntactic sugar over prototypes:

class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }
  
  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

const user = new User('Alice', 'alice@example.com');

Favor composition over inheritance. Instead of deep inheritance hierarchies, compose objects from smaller pieces:

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

const canWalk = {
  walk() {
    console.log('Walking...');
  }
};

const person = { name: 'Alice', ...canEat, ...canWalk };
person.eat('pizza'); // 'Eating pizza'
person.walk(); // 'Walking...'

Use destructuring in function parameters for cleaner APIs:

function createServer({ port = 3000, host = 'localhost', ssl = false }) {
  console.log(`Server starting on ${host}:${port} (SSL: ${ssl})`);
}

createServer({ port: 8080, ssl: true });

Objects excel as configuration parameters. They’re self-documenting and order-independent:

// Bad: positional parameters are unclear
function fetch(url, true, 5000, false, 'json');

// Good: object parameter is clear
function fetch(url, {
  cache = true,
  timeout = 5000,
  credentials = false,
  responseType = 'json'
} = {}) {
  // Implementation
}

fetch('/api/users', { timeout: 10000, responseType: 'text' });

Understanding objects deeply—their properties, methods, and prototype chain—is essential for effective JavaScript development. These patterns form the foundation of the language and enable you to write cleaner, more maintainable code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.