A classe `Object` no JavaScript

A classe `Object` no JavaScript

Este artigo explica a classe Object no JavaScript.

Este artigo explica a classe Object no JavaScript, incluindo exemplos práticos.

YouTube Video

A classe Object no JavaScript

Object é um objeto embutido que serve como base para todos os objetos JavaScript. Muitos dos recursos principais da linguagem, como gerenciamento de propriedades, herança (cadeia de protótipos), enumeração, clonagem e congelamento, são fornecidos através do comportamento do Object.

Object.create

Existem várias formas de criar objetos, e você deve utilizá-las apropriadamente dependendo do seu objetivo.

Literal de objeto (mais comum)

O código a seguir mostra a forma mais simples e legível de criar um 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"
  • Neste exemplo, propriedades e métodos são definidos usando literais. É simples e geralmente oferece desempenho superior.

Construtor new Object()

O construtor Object é raramente utilizado, mas é útil entender o seu comportamento.

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() retorna um objeto vazio, mas o literal {} é mais curto e mais comum.

Especificando o protótipo com Object.create

Object.create é usado para criar um objeto com um protótipo 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 é ideal para design de objetos baseados em herança, permitindo o controle preciso da cadeia de protótipos.

Atributos e descritores de propriedades

Propriedades possuem atributos como 'value', 'writable', 'enumerable' e 'configurable', os quais podem ser controlados em detalhe com Object.defineProperty.

Exemplo básico de uso de defineProperty

A seguir, um exemplo de definição de propriedades não enumeráveis e somente leitura usando 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
  • O uso de defineProperty permite controlar precisamente o comportamento das propriedades, como enumeração, reescrita e exclusão.

Propriedades de acesso (getter / setter)

Com acessores, você pode injetar lógica nas leituras e escritas de propriedades.

 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
  • Com getter e setter, você pode tratar o acesso à propriedade como uma API externa e adicionar validação ou efeitos colaterais.

Protótipo e herança (prototype / __proto__ / Object.getPrototypeOf)

A herança em JavaScript é baseada na cadeia de protótipos, não em classes. Objetos podem referenciar outros objetos como seus protótipos.

Object.getPrototypeOf e Object.setPrototypeOf

O exemplo a seguir mostra como inspecionar e definir protótipos.

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 o protótipo de um objeto.
  • Object.setPrototypeOf altera o protótipo de um objeto existente, mas deve ser usado com cautela pois pode afetar a performance.

Métodos internos importantes

Explicaremos claramente os métodos mais usados e importantes selecionados entre os métodos de instância fornecidos por Object.prototype, assim como os métodos estáticos pertencentes a Object.

hasOwnProperty, isPrototypeOf, toString, valueOf

hasOwnProperty, isPrototypeOf, toString e valueOf definem o comportamento básico dos 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 é um método essencial para verificar se uma propriedade está diretamente presente no objeto.
  • isPrototypeOf verifica se o objeto alvo tem a si mesmo como protótipo.

Object.keys, Object.values, Object.entries

Object.keys, Object.values e Object.entries retornam listas das propriedades enumeráveis próprias de um objeto. Eles são úteis para iteração e transformação.

 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));
  • Eles são frequentemente usados para iterar e transformar objetos.

Object.assign

Object.assign é usado para cópia rasa e mesclagem. Observe que protótipos e propriedades de acesso não são copiadas.

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)
  • Para objetos aninhados, apenas as referências são copiadas, então é necessário uma implementação diferente para clonagem profunda.

Object.freeze, Object.seal, Object.preventExtensions

Object.freeze, Object.seal e Object.preventExtensions controlam a mutabilidade dos 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 é o mais rigoroso; ele impede qualquer alteração nas propriedades do objeto.
  • seal impede a adição ou exclusão de propriedades, mas permite a alteração dos valores das propriedades existentes.
  • preventExtensions apenas impede a adição de novas propriedades; as propriedades existentes ainda podem ser alteradas ou excluídas.

Enumerabilidade, ordem e laços for...in / for...of de objetos

for...in enumera nomes de propriedades enumeráveis, mas também inclui propriedades na cadeia de protótipos, por isso costuma ser usado em conjunto com hasOwnProperty. Combinar Object.keys() com for...of é mais seguro e deixa suas intenções mais claras.

 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}
  • As regras para enumeração de propriedades são definidas pela especificação ECMAScript, e há casos em que a ordem de enumeração é garantida e outros em que não é. Como regra geral, as chaves interpretadas como números são ordenadas em ordem crescente, enquanto as demais seguem a ordem de inserção.

Clonagem e cópia profunda

Existem dois tipos de cópia de objetos: cópia superficial usando Object.assign ou a sintaxe de spread, e cópia profunda usando métodos recursivos. É importante usá-los de forma apropriada dependendo da situação.

Cópia rasa (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
  • Com a cópia rasa, objetos aninhados compartilham referências, então alterações no objeto original podem afetar a cópia.

Cópia profunda simples (com ressalvas)

Usar o truque do JSON é uma forma rápida de criar uma cópia profunda, mas possui desvantagens como perda de funções, Date, referências circulares e valores undefined. Para clonagem profunda real, é necessário usar uma 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
  • Métodos baseados em JSON são convenientes para manipular rapidamente dados simples, mas em situações comuns, seu comportamento pode falhar.

Mixins e composição de objetos

Em vez de herança múltipla, é comum usar o padrão de composição de comportamentos com 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"
  • Mixins são flexíveis, mas é preciso ter cuidado com colisões de nomes de propriedades e testabilidade.

Armadilhas comuns e melhores práticas

Aqui estão algumas armadilhas comuns e melhores práticas.

  • Mutabilidade Objetos são mutáveis por padrão. Em aplicações com gerenciamento de estado, considere estruturas de dados imutáveis usando Object.freeze ou uma biblioteca imutável.

  • Poluição do protótipo Mesclar dados externos em um objeto diretamente com Object.assign ou laços pode causar efeitos colaterais inesperados com propriedades especiais como __proto__ ou constructor, criando riscos de segurança. Filtre a entrada do usuário antes de mesclá-la diretamente.

  • Armadilhas do for...in for...in também enumera propriedades do protótipo, então verifique com hasOwnProperty. Usar Object.keys é mais claro.

  • Uso indevido de cópia rasa Considere se você precisa de cópia profunda para evitar que alterações em objetos aninhados afetem o objeto original.

Exemplo prático: Padrão de atualização de objeto imutável

Padrões que retornam um novo objeto sem modificar diretamente o estado são comumente usados em React e 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 é um exemplo de criação de um novo objeto de estado sem modificar diretamente o objeto state original. A função toggleTodo copia o array todos e retorna um novo objeto com apenas o elemento alvo modificado, assim o objeto state original permanece inalterado.
  • Atualizações imutáveis reduzem efeitos colaterais e facilitam o gerenciamento de estado.

Exemplo prático: Mesclagem segura (Cuidado com poluição de protótipos)

Ao mesclar JSON externo, ignore __proto__ para evitar poluição de protótipo.

 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
  • Esse tipo de proteção também é importante em bibliotecas e frameworks.

Considerações de performance

Em relação à performance, considere os seguintes pontos:.

  • Evite alterações frequentes de protótipo (Object.setPrototypeOf) ou adição/remoção dinâmica de propriedades, pois isso prejudica otimizações do engine.
  • Ao gerar muitos objetos pequenos, a otimização se torna mais eficaz se você usar objetos com uma estrutura uniforme (o mesmo conjunto de propriedades).
  • Cópia profunda é custosa. Minimize seu uso ou considere atualizações baseadas em diferença.

Resumo

Object é central para o JavaScript, fornecendo uma variedade de recursos como criação de objetos, controle de propriedades, herança, cópia e gerenciamento de mutabilidade. É importante entender APIs como Object.defineProperty, Object.assign e Object.freeze, e projetar com cuidado para evitar armadilhas como poluição de protótipos e cópia superficial.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video