5 formas de se criar objetos no Javascript!

11 minuto(s) de leitura

O Javascript é uma linguagem orientada a objetos muito flexível. E um dos motivos para isso é seu forte paradigma funcional, tão importante quanto o primeiro. Por conta disso, existe mais de uma forma de criarmos objetos e lidarmos com herança e polimorfismo.

Hoje eu vou mostrar as cinco formas que temos de criarmos objetos em Javascript e as particularidades de cada uma delas.

Singleton Objects

Objetos em javascript são em sua essência, bem simples. São basicamente uma variável que armazena uma estrutura de chaves e valores:

var obj = {
  chave1: "valor1",
  chave2: "valor2",
  chave3: "valor3",
  ...
}

Mas, além de valores comuns, é possível armazenar outros objetos ou mesmo funções.

var pessoa = {
  nome: "João",
  idade: 25,
  nascimento: "01/08/1992"
}

var carro = {
  cor: "Vermelho",
  ano: 2007,
  dono: pessoa,
  ligar: function () {
    return this.dono + " ligou o carro"
  }
  andar: function () {
    return "O carro " + this.cor + " está andando"
  }
}

Factory Functions

A forma mais simples de criar um objeto é utilizando a sintaxe acima. Geralmente ela é utilizada quando teremos apenas um objeto do tipo criado. Outra forma, é utilizando funções construtoras:

function Pessoa (nome, idade, nascimento) {
  this.nome = nome
  this.idade = idade
  this.nascimento = nascimento
}

function Carro (cor, ano, dono) {
  this.cor = cor
  this.ano = ano
  this.dono = dono
  this.ligar = function () {
    return this.dono + " ligou o carro"
  }
  this.andar = function () {
    return "O carro " + this.cor + " está andando"
  }
}

var joao = new Pessoa("João", 25, "01/08/1992")
var camaro = new Carro("Amarelo", 2016, joao)
var fusca = new Carro("Preto", 1971, joao)

fusca.cor // Preto
camaro.cor // Amarelo

Funções construtoras são semelhantes aos construtores das classes de outras linguagens. Quando utilizamos o new em uma função construtora, ela cria uma nova instância do objeto com os parâmetros passados a ela. Cada objeto criado com o new é completamente independente do outro.

O problema com as funções construtoras é que além de copiar os seus atributos, ela também copia todas as suas funções a cada objeto criado. Objetos criados assim não possuem funções compartilhadas e acabam gerando um espaço desnecessário toda vez que criamos um novo objeto.

Afinal, porque não podemos compartilhar as funções ligar e andar do objeto Carro se cada instância é independente da outra, não causando nenhum problema?

Prototypes

Para resolver este problema, podemos utilizar os Prototypes. No Javascript, todas as funções possuem um objeto chamado prototype, onde podemos adicionar métodos ou objetos que serão compartilhados entre todas as instâncias criadas com o new. Veja um exemplo:

function Carro (cor, ano, dono) {
  this.cor = cor
  this.ano = ano
  this.dono = dono
}

Carro.prototype.ligar = function () {
  return this.dono + " ligou o carro"
}

Carro.prototype.andar = function () {
  return "O carro " + this.cor + " está andando"
}

var camaro = new Carro("Amarelo", 2016, joao)
var fusca = new Carro("Preto", 1977, joao)

camaro.andar() // "O carro amarelo está andando
fusca.andar()  // O carro preto está andando

Agora, os objetos camaro e fusca não possuem mais, cada um, uma cópia dos métodos definidos na função construtora. Os dois estão utilizando os mesmos métodos definidos no protótipo de Carro e mesmo assim ainda retornam valores diferentes. Isso acontece pois o this utilizado sempre é o do objeto que invocou a função, conforme já expliquei com mais detalhes aqui.

O protótipo de uma função serve justamente para compartilharmos métodos e objetos entre instâncias. Todos os objetos possuem um protótipo associado a ele. Se não modificamos esse protótipo, ele será o próprio Object do Javascript.

Cadeia de Protótipos

Quando analisamos o objeto pessoa criado no início e que não definimos um protótipo, por exemplo, sua estrutura interna será a seguinte:

image-center

Perceba que junto com os atributos que havíamos definido no início, ele também possui o atributo __proto__. Todos os objetos criados possuem esse atributo, até mesmo um objeto vazio como {}.

O atributo __proto__ sempre vai armazenar uma referência a um objeto que está um nível acima na hierarquia de protótipos. Se o objeto criado não herdar de nenhum outro, então __proto__ referenciará ao Object.

Quando criamos um objeto a partir da função construtora de Carro, que possui métodos definidos no seu protótipo, a estrutura fica um pouco diferente:

image-center

Perceba que agora, apenas os atributos definidos no construtor da função são propriedades da instância criada. Os métodos andar e ligar aparecem dentro de __proto__, que está referenciando o protótipo do Carro, que por sua vez, também possui o atributo __proto__, que referencia a Object.

Esse conjunto de referências que sempre vai subindo na hierarquia, chama-se cadeia de protótipos e é um dos fundamentos de como funciona a orientação a objetos no Javascript.

Sempre que queremos o valor de um atributo em um objeto, o interpretador primeiro vai buscar nos atributos do próprio objeto. Caso não encontre, ele procura dentro do seu protótipo e caso não encontre, vai sempre subindo, até chegar no Object do Javacript. Se esse objeto não existir em nenhum nível da cadeia de protótipos, é retornado o valor undefined.

Quando queremos iterar sobre todas os atributos da cadeia de protótipos, podemos utilizar o for para isso:

var props = []

for(i in camaro) {
  props.push(i)
}

console.log(props)
// ["cor", "ano", "dono", "ligar", "andar"]

E se quisermos iterar apenas sobre os atributos exclusivos desse objeto, utilizamos o método hasOwnProperty:

var props = []
for(i in camaro) {
  if(camaro.hasOwnProperty(i))
    props.push(i)
}

console.log(props) // ["cor", "ano", "dono"]

Object.create

Se quisermos criar um novo objeto tendo como protótipo direto seu, outro objeto, podemos utilizar a função Object.create():

var camaro2 = Object.create(camaro)
camaro2.cor // Amarelo
camaro2.ano // 2016

Agora veja a estrutura interna do objeto camaro2:

image-center

O objeto camaro2 em si não possui nenhum atributo, apenas o seu protótipo que está referenciando o objeto camaro. Se mudarmos a cor em camaro2, como ele não possui esse atributo, ele será criado e irá se sobrepor ao atributo cor do objeto camaro:

camaro2.cor = "Azul"
console.log(camaro2.cor) // Azul

Agora veja como fica a estrutura do objeto camaro2:

image-center

Perceba que o atributo cor agora faz parte do próprio objeto camaro2, mas ele não substitui o atributo do objeto referenciado. No entanto, como ele está no primeiro nível da cadeia de protótipos, ele sempre será consultado primeiro. O problema é que se quisermos deletar mais tarde o atributo cor, apenas o atributo do primeiro nível na cadeia será deletado, e o objeto camaro2 voltará a ter cor = "Amarelo".

Propriedades de atributos

Para resolver esse problema, temos que utilizar o segundo parâmetro da função Object.create(), e definirmos todos os atributos que queremos modificar nela.

var camaro3 = Object.create(camaro, {
  cor: {
    value: "Azul",
    writable: true,
    configurable: false,
    enumerable: true
  },
  ano: {
    value: 1800,
    writable: false,
    configurable: true,
    enumerable: false
  }
})

Se você ainda não ouviu falar sobre as propriedades de atributos, esse trecho de código poderá parecer um pouco confuso pra você. Mas não é complicado.

Cada atributo de um objeto pode ter propriedades como, possibilidade de ter seu atributo alterado, ser enumerável e possibilidade de ser excluído ou ter suas propriedades alteradas. Veja a explicação de cada propriedade abaixo:

  • value: é o valor que o atributo receberá.
  • writable: indica se o valor poderá ser reescrito ou, depois de ter seu valor definido, será imutável.
  • configurable: define se o atributo poderá ser excluido do objeto, ou se poderemos ou não modificar as suas propriedades.
  • enumerable: define se o atributo será contado quando utilizarmos o for ou Object.keys()

Veja como fica a estrutura do objeto camaro3 depois que definimos suas propriedades:

image-center

No exemplo acima, definimos propriedades distintas para os atributos cor e ano do objeto camaro3.

Para o atributo cor, definimos que poderemos alterar o seu valor, que não poderemos mais excluí-lo do objeto e nem modificar mais nenhuma propriedade sua e que poderemos iterar sobre ele com o for ou Object.keys().

Para o atributo ano, definimos que ele não poderá ser reescrito, que poderemos modificar suas propriedades e até excluí-lo e que não será contado em uma iteração.

Veja um exemplo:

// Tentando iterar
var props = []
for(i in camaro3)
  props.push(i)
console.log(props) // ["cor", "dono", "ligar", "andar"] 

// Tentando alterar o valor
camaro3.cor = "Branco"
console.log(camaro3.cor) // Branco
camaro3.ano = 2016
console.log(camaro3.ano) // 1800

// Tentando deletar
console.log(camaro3.cor) // Azul
delete camaro3.cor // false
console.log(camaro3.cor) // Azul

console.log(camaro3.ano) // 1800
delete camaro3.ano // true
console.log(camaro3.ano) // undefined

Perceba que o atributo ano não foi mais visível pelo for e também não fomos capaz de alterar o seu valor, e o atributo cor mesmo que possa ter o seu valor alterado, não pode ser excluído com o delete.

Esse é o poder das propriedades, dar a possibilidade de deixar o seu código um pouco menos flexível do que a linguagem permite. Para mais detalhes sobre como alterar propriedades no Javascript, veja esse link.

Classes

A partir do ECMAScript 6, podemos criar objetos definindo classes no modo clássico da orientação a objetos. Na verdade, a nova keyword class é apenas uma função especial feita para agradar desenvolvedores que vieram de linguagens como Java ou C#. Isso porque ela é apenas uma Syntax Sugar para o modelo de protótipos que já mostrei acima. A forma que podemos utilizar agora para definir protótipos com a keyword class é:

class Carro {
  constructor(cor, ano, dono) {
    this.cor = cor
    this.ano = ano
    this.dono = dono
  }

  ligar() {
    return this.dono + " ligou o carro"
  }

  andar() {
    return "O carro " + this.cor + " está andando"
  }
}

var palio = new Carro("Prata", 2007, joao)

Se olharmos a estrutura interna desse objeto criado a partir de uma class, veremos que é a mesma de um objeto criado com funções construtoras e protótipos:

image-center

Com isso totalizamos cinco maneiras diferentes de se criar objetos no Javascript. Quanta flexibilidade hein! Nos próximos artigos vou tratar sobre as formas de se implementar herança no Javascript. Isso mesmo, no plural.

Se você gostou do meu blog e quer ler mais artigos que eu posto, inscreva-se no meu feed. Eu sempre procuro postar artigos semanais aqui.

Um grande abraço!

Deixe um comentário