La clase `Object` en JavaScript

La clase `Object` en JavaScript

Este artículo explica la clase Object en JavaScript.

Este artículo explica la clase Object en JavaScript, incluyendo ejemplos prácticos.

YouTube Video

La clase Object en JavaScript

Object es un objeto incorporado que sirve como base para todos los objetos en JavaScript. Muchas de las características principales del lenguaje, como la gestión de propiedades, la herencia (cadena de prototipos), la enumeración, la clonación y el congelamiento, se proporcionan a través del comportamiento de Object.

Object.create

Hay varias formas de crear objetos, y debes usarlas adecuadamente dependiendo de tu propósito.

Literal de objeto (la más común)

El siguiente código muestra la forma más simple y legible de crear un objeto.

 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"
  • En este ejemplo, las propiedades y los métodos se definen usando literales. Es sencillo y generalmente ofrece un rendimiento superior.

Constructor new Object()

El constructor Object se usa raramente, pero es útil comprender su comportamiento.

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() devuelve un objeto vacío, pero el literal {} es más corto y más común.

Especificar el prototipo con Object.create

Object.create se utiliza para crear un objeto con un prototipo especificado.

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 es ideal para el diseño de objetos basado en herencia, permitiendo controlar precisamente la cadena de prototipos.

Atributos de propiedades y descriptores

Las propiedades tienen atributos como 'value', 'writable', 'enumerable', y 'configurable', los cuales se pueden controlar en detalle con Object.defineProperty.

Ejemplo básico de uso de defineProperty

A continuación, un ejemplo de cómo definir propiedades no enumerables y de solo lectura utilizando 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
  • Usar defineProperty permite controlar con precisión el comportamiento de las propiedades, como la enumeración, la sobrescritura y la eliminación.

Propiedades accesoras (getter / setter)

Con los accesores, puedes inyectar lógica en la lectura y escritura de propiedades.

 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
  • Con getter y setter, puedes tratar el acceso a propiedades como una API externa y añadir validaciones o efectos secundarios.

Prototipo y herencia (prototype / __proto__ / Object.getPrototypeOf)

La herencia en JavaScript se basa en la cadena de prototipos, no en las clases. Los objetos pueden referirse a otros objetos como sus prototipos.

Object.getPrototypeOf y Object.setPrototypeOf

El siguiente ejemplo muestra cómo inspeccionar y establecer prototipos.

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 recupera el prototipo de un objeto.
  • Object.setPrototypeOf cambia el prototipo de un objeto existente, pero debes usarlo cuidadosamente ya que puede afectar el rendimiento.

Métodos incorporados importantes.

Explicaremos claramente los métodos más utilizados y relevantes, seleccionados de los métodos de instancia proporcionados por Object.prototype, así como los métodos estáticos que posee Object.

hasOwnProperty, isPrototypeOf, toString, valueOf

hasOwnProperty, isPrototypeOf, toString y valueOf definen el comportamiento básico de los objetos.

 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 es un método esencial para comprobar si una propiedad está directamente en el objeto.
  • isPrototypeOf verifica si el objeto de destino lo tiene a sí mismo como prototipo.

Object.keys, Object.values, Object.entries

Object.keys, Object.values y Object.entries devuelven listas de las propiedades enumerables propias de un objeto. Son útiles para la iteración y la transformación.

 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));
  • Estos se usan frecuentemente para iterar y transformar objetos.

Object.assign

Object.assign se utiliza para copiar superficialmente y para fusionar objetos. Ten en cuenta que los prototipos y las propiedades accesoras no se copian.

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)
  • En objetos anidados, solo se copian las referencias, así que necesitas una implementación diferente para la clonación profunda.

Object.freeze, Object.seal, Object.preventExtensions

Object.freeze, Object.seal y Object.preventExtensions controlan la mutabilidad de los objetos.

 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 es el más estricto; previene cualquier cambio en las propiedades del objeto.
  • seal impide agregar o eliminar propiedades, pero permite cambiar los valores de las propiedades existentes.
  • preventExtensions solo evita la adición de nuevas propiedades; las propiedades existentes aún se pueden cambiar o eliminar.

Enumerabilidad de objetos, orden y for...in / for...of

for...in enumera los nombres de las propiedades enumerables, pero también incluye propiedades en la cadena de prototipos, por lo que a menudo se usa en combinación con hasOwnProperty. Combinar Object.keys() con for...of es más seguro y aclara tus intenciones.

 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}
  • Las reglas para la enumeración de propiedades están definidas en la especificación ECMAScript, y hay casos donde el orden de enumeración está garantizado y otros donde no lo está. Como regla general, las claves interpretadas como números se ordenan en orden ascendente, mientras que las demás claves siguen el orden de inserción.

Clonación y copiado profundo

Hay dos tipos de copiado de objetos: copiado superficial usando Object.assign o la sintaxis de propagación, y copiado profundo usando métodos recursivos. Es importante usarlos apropiadamente según la situación.

Copiado superficial (sintaxis de propagación / 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
  • Con el copiado superficial, los objetos anidados comparten referencias, por lo que los cambios en el objeto original pueden afectar la copia.

Copia profunda simple (con advertencias)

Usar el truco de JSON es una forma rápida de realizar una copia profunda, pero tiene desventajas como la pérdida de funciones, objetos Date, referencias circulares y valores undefined. Para una clonación profunda real, necesitas usar una biblioteca dedicada.

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
  • Los métodos basados en JSON son convenientes para manejar rápidamente datos simples, pero en casos comunes su comportamiento puede fallar.

Mixins y composición de objetos

En lugar de herencia múltiple, a menudo se usa el patrón de componer comportamientos mediante mixins.

 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"
  • Los mixins son flexibles, pero debes tener cuidado con las colisiones de nombres de propiedades y la capacidad de prueba.

Errores comunes y buenas prácticas

Aquí hay algunos errores comunes y buenas prácticas.

  • Mutabilidad Los objetos son mutables por defecto. En aplicaciones con gestión de estado, considera estructuras de datos inmutables usando Object.freeze o una biblioteca inmutable.

  • Contaminación del prototipo Fusionar datos externos en un objeto directamente con Object.assign o bucles puede causar efectos secundarios inesperados con propiedades especiales como __proto__ o constructor, creando riesgos de seguridad. Filtra la entrada del usuario antes de fusionarla directamente.

  • Peligros de for...in for...in también enumera propiedades del prototipo, así que verifica con hasOwnProperty. Usar Object.keys es más claro.

  • Mal uso del copiado superficial Considera si necesitas copiado profundo para evitar que los cambios en objetos anidados afecten al objeto original.

Ejemplo Práctico: Patrón de Actualización de Objeto Inmutable

Los patrones que devuelven un nuevo objeto sin modificar directamente el estado se usan comúnmente en React y bibliotecas similares.

 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
  • Este código es un ejemplo de cómo crear un nuevo objeto de estado sin modificar directamente el objeto state original. La función toggleTodo copia el arreglo todos y devuelve un nuevo objeto con solo el elemento objetivo modificado, de modo que el objeto state original permanece sin cambios.
  • Las actualizaciones inmutables reducen los efectos secundarios y facilitan la gestión del estado.

Ejemplo Práctico: Fusión Segura (Cuidado con la Contaminación de Prototipo)

Al fusionar JSON externo, ignora __proto__ para evitar la contaminación del prototipo.

 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
  • Este tipo de protección también es importante en bibliotecas y frameworks.

Consideraciones de rendimiento

Con respecto al rendimiento, considera los siguientes puntos:.

  • Evita cambios frecuentes de prototipo (Object.setPrototypeOf) o adición/eliminación dinámica de propiedades, ya que esto dificulta la optimización del motor.
  • Al generar muchos objetos pequeños, la optimización se vuelve más efectiva si usas objetos con una estructura uniforme (el mismo conjunto de propiedades).
  • La clonación profunda es costosa. Minimiza su uso o considera utilizar actualizaciones basadas en diferencias.

Resumen

Object es fundamental en JavaScript, proporcionando una variedad de características como creación de objetos, control de propiedades, herencia, copiado y gestión de la mutabilidad. Es importante comprender APIs como Object.defineProperty, Object.assign y Object.freeze, y diseñar con cuidado para evitar problemas como la contaminación del prototipo y la copia superficial.

Puedes seguir el artículo anterior utilizando Visual Studio Code en nuestro canal de YouTube. Por favor, también revisa nuestro canal de YouTube.

YouTube Video