The `Object` class in JavaScript

The `Object` class in JavaScript

This article explains the Object class in JavaScript.

This article explains the Object class in JavaScript, including practical examples.

YouTube Video

The Object class in JavaScript

Object is a built-in object that serves as the base for all JavaScript objects. Many of the language's core features, such as property management, inheritance (prototype chain), enumeration, cloning, and freezing, are provided through the behavior of Object.

Object.create

There are multiple ways to create objects, and you should use them appropriately depending on your purpose.

Object literal (most common)

The following code shows the simplest and most readable way to create an object.

 1// Create an object using object literal
 2const user = {
 3  name: "Alice",
 4  age: 30,
 5  greet() {
 6    return `Hello, I'm ${this.name}`;
 7  }
 8};
 9
10console.log(user.greet()); // "Hello, I'm Alice"
  • In this example, properties and methods are defined using literals. It is simple and generally offers superior performance.

new Object() constructor

The Object constructor is rarely used, but it's helpful to understand its behavior.

1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
  • new Object() returns an empty object, but the literal {} is shorter and more common.

Specifying the prototype with Object.create

Object.create is used to create an object with a specified prototype.

1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
  • Object.create is ideal for inheritance-based object design, allowing you to precisely control the prototype chain.

Property attributes and descriptors

Properties have attributes like 'value', 'writable', 'enumerable', and 'configurable', which can be controlled in detail with Object.defineProperty.

Basic example of using defineProperty

Next is an example of defining non-enumerable and read-only properties using defineProperty.

 1// Define a non-enumerable read-only property
 2const person = { name: "Carol" };
 3
 4Object.defineProperty(person, "id", {
 5  value: 12345,
 6  writable: false,
 7  enumerable: false,
 8  configurable: false
 9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
  • Using defineProperty allows you to precisely control property behavior such as enumeration, rewriting, and deletion.

Accessor properties (getter / setter)

With accessors, you can inject logic into property reads and writes.

 1// Use getter and setter to manage internal state
 2const data = {
 3  _value: 1,
 4  get value() {
 5    return this._value;
 6  },
 7  set value(v) {
 8    if (typeof v === "number" && v > 0) {
 9      this._value = v;
10    } else {
11      throw new Error("value must be a positive number");
12    }
13  }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
  • With getter and setter, you can treat property access like an external API and add validation or side effects.

Prototype and inheritance (prototype / __proto__ / Object.getPrototypeOf)

Inheritance in JavaScript is based on the prototype chain, not on classes. Objects can refer to other objects as their prototypes.

Object.getPrototypeOf and Object.setPrototypeOf

The following example shows how to inspect and set prototypes.

1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
  • Object.getPrototypeOf retrieves the prototype of an object.
  • Object.setPrototypeOf changes the prototype of an existing object, but you should use it carefully since it can affect performance.

Important built-in methods

We will clearly explain the most commonly used and important methods selected from the instance methods provided by Object.prototype, as well as the static methods owned by Object.

hasOwnProperty, isPrototypeOf, toString, valueOf

hasOwnProperty, isPrototypeOf, toString, and valueOf define the basic behavior of objects.

 1// Demonstrate prototype methods
 2const base = { greet() { return "hello"; } };
 3const child = Object.create(base);
 4const a = { x: 1 };
 5
 6console.log(a.hasOwnProperty("x")); // true
 7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
 8
 9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
  • hasOwnProperty is an essential method for checking if a property is directly on the object.
  • isPrototypeOf checks whether the target object has itself as a prototype.

Object.keys, Object.values, Object.entries

Object.keys, Object.values, and Object.entries return lists of an object's own enumerable properties. They are useful for iteration and transformation.

 1// Keys, values and entries
 2const item = { id: 1, name: "Widget", price: 9.99 };
 3
 4// ["id", "name", "price"]
 5console.log(Object.keys(item));
 6
 7// [1, "Widget", 9.99]
 8console.log(Object.values(item));
 9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));
  • These are frequently used for iterating and transforming objects.

Object.assign

Object.assign is used for shallow copying and merging. Note that prototypes and accessor properties are not copied.

1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
  • For nested objects, only the references are copied, so you need a different implementation for deep cloning.

Object.freeze, Object.seal, Object.preventExtensions

Object.freeze, Object.seal, and Object.preventExtensions control the mutability of objects.

 1// Freeze vs seal vs preventExtensions
 2const obj = { a: 1 };
 3Object.freeze(obj);
 4obj.a = 2; // fails silently or throws in strict mode
 5delete obj.a; // fails
 6
 7const obj2 = { b: 2 };
 8Object.seal(obj2);
 9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
  • freeze is the strictest; it prevents any changes to the object's properties.
  • seal prevents adding or deleting properties, but allows changing the values of existing properties.
  • preventExtensions only prevents the addition of new properties; existing properties can still be changed or deleted.

Object enumerability, order, and for...in / for...of

for...in enumerates enumerable property names, but it also includes properties on the prototype chain, so it's often used in combination with hasOwnProperty. Combining Object.keys() with for...of is safer and makes your intentions clearer.

 1// Safe enumeration
 2const obj = Object.create({ inherited: true });
 3obj.own = 1;
 4
 5for (const key in obj) {
 6  if (obj.hasOwnProperty(key)) {
 7    console.log("own prop:", key);
 8  } else {
 9    console.log("inherited prop:", key);
10  }
11}
12
13for (const key of Object.keys(obj)) {
14  console.log("key via Object.keys:", key);
15}
  • The rules for property enumeration are defined in the ECMAScript specification, and there are cases where the enumeration order is guaranteed and others where it is not. As a general rule, keys interpreted as numbers are ordered in ascending order, while other keys follow the insertion order.

Cloning and deep copying

There are two types of object copying: shallow copying using Object.assign or the spread syntax, and deep copying using recursive methods. It is important to use them appropriately depending on the situation.

Shallow copying (spread syntax / Object.assign)

1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
  • With shallow copying, nested objects share references, so changes in the original object may affect the copy.

Simple deep copy (with caveats)

Using the JSON trick is a quick way to create a deep copy, but it has drawbacks such as losing functions, Date, circular references, and undefined values. For true deep cloning, you need to use a dedicated library.

1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
  • JSON-based methods are convenient for quickly handling simple data, but in common cases, their behavior can break.

Mixins and object composition

Instead of multiple inheritance, the pattern of composing behaviors using mixins is often used.

 1// Simple mixin function
 2const canEat = {
 3  eat() { return `${this.name} eats`; }
 4};
 5const canWalk = {
 6  walk() { return `${this.name} walks`; }
 7};
 8
 9function createPerson(name) {
10  const person = { name };
11  return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
  • Mixins are flexible, but you need to be careful about property name collisions and testability.

Common pitfalls and best practices

Here are some common pitfalls and best practices.

  • Mutability Objects are mutable by default. In applications with state management, consider immutable data structures using Object.freeze or an immutable library.

  • Prototype pollution Merging external data into an object directly with Object.assign or loops can cause unexpected side effects with special properties such as __proto__ or constructor, creating security risks. Filter user input before directly merging it.

  • Pitfalls of for...in for...in enumerates prototype properties as well, so check with hasOwnProperty. Using Object.keys is clearer.

  • Misuse of shallow copy Consider whether you need deep copying to prevent changes in nested objects from affecting the original object.

Practical Example: Immutable Object Update Pattern

Patterns that return a new object without directly modifying the state are commonly used in React and similar libraries.

 1// Immutable update example
 2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
 3
 4// Toggle todo done immutably
 5function toggleTodo(state, todoId) {
 6  return {
 7    ...state,
 8    todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
 9  };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
  • This code is an example of creating a new state object without directly modifying the original state object. The toggleTodo function copies the todos array and returns a new object with only the target element modified, so the original state remains unchanged.
  • Immutable updates reduce side effects and make state management easier.

Practical Example: Safe Merging (Be Careful of Prototype Pollution)

When merging external JSON, ignore __proto__ to prevent prototype pollution.

 1// Safe merge ignoring __proto__ keys
 2function safeMerge(target, source) {
 3  for (const key of Object.keys(source)) {
 4    if (key === "__proto__" || key === "constructor") continue;
 5    target[key] = source[key];
 6  }
 7  return target;
 8}
 9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
  • This kind of protection is also important in libraries and frameworks.

Performance considerations

With regard to performance, consider the following points:.

  • Avoid frequent prototype changes (Object.setPrototypeOf) or dynamic addition/removal of properties, as these hinder engine optimizations.
  • When generating many small objects, optimization becomes more effective if you use objects with a uniform structure (the same set of properties).
  • Deep copying is costly. Minimize their use or consider using diff-based updates.

Summary

Object is central to JavaScript, providing a variety of features such as object creation, property control, inheritance, copying, and mutability management. It is important to understand APIs such as Object.defineProperty, Object.assign, and Object.freeze, and to design with care to avoid pitfalls like prototype pollution and shallow copying.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video