Skip to content

Instantly share code, notes, and snippets.

@ja0n
Last active August 29, 2015 14:19
Show Gist options
  • Save ja0n/70c7036084afd40196fd to your computer and use it in GitHub Desktop.
Save ja0n/70c7036084afd40196fd to your computer and use it in GitHub Desktop.
Um resumão sobre objetos em Javascript

#(Quase) Tudo que você precisa saber sobre objetos em Javascript

Roteiro

  1. Javascript é sobre objetos
  2. Declaração
  3. Propriedades e descritores
  4. Métodos e o this
  5. Notação literal
  6. Herança e protótipos
  7. Construtores e o new
  8. Considerações

Futuro

  • Closures
  • Encapsulamento
  • ES 6 (Harmony)

##1. Javascript é sobre objetos     Objetos em Javascript são simplesmente um conjunto de chaves (únicas) apontando para um valor, ou seja, uma lista não ordenada de chave-valor. É importante perceber que tudo que você manipula em Javascript é objeto. O que não for objeto, é primitivo (que são convertidos para objeto em algumas situações). Strings, booelans e numbers têm seus respectivos empacotadores.

    Também é trivial notar que os objetos aqui não dependem de camadas de metadados (classes, por exemplo) e podem ter suas propriedades alteradas em qualquer momento do tempo de execução. Então, antes de continuar a leitura, guarde os seus conceitos de orientação a objetos na gaveta, Javascript tem uma abordagem diferente (mais fiél, mas isso é assunto para outra vida), abram espaço para uma palavra nova: protótipo.

    Como o título desta seção alerta, Javascript é sobre objetos, logo, entender a dinâmica dos objetos aqui é também entender Javascript.

##2. Declaração     Para criar um objeto vazio (sem parente ou propriedades) usamos a seguinte:

var obj = Object.create(null);

    O método Object.create recebe dois parâmetros: um protótipo e um conjunto de descritores (este último sendo opcional), respectivamente, e retorna uma instância. Logo, ao passarmos o protótipo como null, será instanciado um objeto sem parente. Não se preocupe com os descritores agora, esse assunto será abordado logo a seguir. Protótipo virá um pouco mais à frente.

##3. Propriedades e descritores

Tá faltando falar do Object.defineProperties

    Agora que sabemos como criar um objeto vazio, está na hora de começarmos a descrever suas características. Para isso usaremos o método Object.defineProperty:

var pessoa = Object.create(null);

Object.defineProperty(pessoa, 'nome', { value: 'Jaon'
                                      , writable: true
                                      , configurable: true
                                      , enumerable: true });

Object.defineProperty(pessoa, 'sobrenome', { value: '0blivion'
                                           , writable: true
                                           });

    Esse método recebe três parâmetrs: a instância, o nome da propriedade (chave) e o descritor, respectivamente. Ou seja, criamos a propriedade nome na instância pessoa com o valor Jaon.

    Sobre descritores, existem dois tipos: descritor de dado e descritor de acesso. Em suma são objetos com propriedades específicas que servem para descrever alguns comportamentos de uma propriedade. As propriedades possíveis são:

  • configurable: se a propriedade pode ser deletada ou ter seu tipo alterado
  • enumerable: se a propriedade será listada em possíveis enumerações
  • value: o valor associado à propriedade
  • writable: se o valor da propriedade poderá ser alterado através de um operador de atribuição
  • get: funciona como um getter. O retorno da função pode ser entendido como o value da propriedade
  • set: functiona como um setter. A função recebe os argumentos passados

    Ambos os descritores (de dado e de acesso) aceitam configurable e enumerable, enquanto value e writable se restringem ao descritor de dado e get e set ao de acesso. Se valores não forem passado, serão considerados como false e undefined, no caso de value, get e set. Também é importante saber que você não pode misturar os dois tipos de descritores na mesma propriedade. Agora vejamos um exemplo mais completo:

function recuperar_nome() {
  return this.first_name + ' ' + this.last_name;
}

function trocar_nome(novo_nome) {
	var nomes = novo_nome.trim().split(/\s+/);
  this.nome = nomes['0'] || '';
  this.sobrenome  = nomes['1'] || '';
}
Object.defineProperty(pessoa, 'nome_completo', { get: recuperar_nome
                                               , set: trocar_nome
                                               , configurable: true
                                               , enumerable: true
                                               });
Object.defineProperty(pessoa, 'idade', { value: '50'
                                       , writable: false
                                       });
pessoa.nome_completo; // "Jaon 0blivion"
pessoa.nome_completo = "zé zão";
pessoa.sobrenome; // "zão"
pessoa.nome; // "zé"
pessoa.idade = 10;
pessoa.idade; // 50

    Reutilizando o objeto criado no início da seção criamos duas funções para atuarem como getter e setter. A primeira retorna os valores das propriedades nome e sobrenome concatenados. A segunda executa operações em uma string para extrair os dois nomes e atribuí-los às respectivas propriedades. Também declaramos uma propriedade idade com valor 50 que não pode ser alterada pelo operador de atribuição (=).

##4. Métodos e o this

Tá faltando falar do Object.apply e do Object.bind que eu acho que fica pra seção de closure

    O método é alguma operação que o objeto pode realizar, ou seja, executa um pedaço de código. E para isto serve uma função: armazenar uma porção de código. Então para criar um método podemos simplesmente declarar uma propriedade lhe atribuindo como valor uma função, parecido com o que fazemos no get e set da seção anterior. Quero aproveitar esta seção para explicar que isso só é possível pois as funções em Javascript são de primeira classe, uma vez que são objetos. Proof of concept:

//~Imaginem aquele objeto lá de cima sendo declarado aqui~~
var cumprimentar = function() {
  console.log('Olá, meu nome é ' + this.nome);
};
Object.defineProperty(pessoa, 'cumprimentar', { value: cumprimentar});

pessoa.cumprimentar() // Olá, meu nome é Jaon

    Também quero aproveitar um pouco para falar sobre algo que deixa muita gente confusa: o this. Bom, o this é uma palavra mágica que pode ser utilizada dentro de funções para se referir a algum objeto (normalmente o que a está invocando). Ao chamarmos a função através de um objeto (como um método), o this será uma referência ao próprio objeto. Ao declararmos uma função no contexto global e a chamarmos lá mesmo, o this será uma referência ao objeto window, no caso dos navegadores, e ao objeto global, no NodeJS. Eles são os contextos globais.

    Como dito no início, função (em Javascript) é um objeto, logo tem lá seus métodos. Vejamos em prática isso tudo que foi dito agora:

var retornarX = function() {
  return this.x;
}
var foo = { x: 5 };
var bar = { x: 10 };

retornarX(); // ReferenceError: x is not defined
var x = 3;
retornarX(); // 3
retornarX.call(foo); // 5
retornarX.call(bar); // 10

    Ainda sobre o this, o call é um método herdado do objeto Function (um empacotador como Number, String e etc) e nos permite explicitar a referência do this, que será o primeiro argumento passado. Aqui foi falado sobre o this em três casos, o quarto e último será falado na seção de construtores.

    Viu alguma coisa estranha na quarta ou quinta linha? Então tem algo especial para você na próxima seção. Run, Forrest, Run!

##5. Notação literal     Bom, tudo o que foi dito até aqui não é mentira e teve um propósito, mas sim, existe um jeito "mais fácil" de trabalhar com objetos. A sintaxe do objeto literal nos permite iniciar um objeto e declarar suas propriedades ao mesmo tempo. Antes de tudo quero deixar claro que um objeto literal sem propriedades não é um objeto vazio pois o objeto literal será uma instância do Object:

var objeto = Object.create(null);
var objeto_literal = {};

objeto instanceof Object //false
objeto_literal instanceof Object //true

// {} = Object.create(Object.prototype)

    Agora vamos revisitar a nossa pessoa:

var pessoa = {
  nome: 'Jaon',
  sobrenome: '0blivion',
  idade: 50,
  get nome_completo() {
    return this.nome + ' ' + this.sobrenome;
  },
  set nome_completo(novo_nome) {
    var nomes = novo_nome.trim().split(/\s+/);
    this.nome = nomes['0'] || '';
    this.sobrenome  = nomes['1'] || '';
  }
};

pessoa.nome_completo; // "Jaon 0blivion"
pessoa.nome_completo = "zé zão";
pessoa.sobrenome; // "zão"
pessoa.nome; // "zé"
pessoa.idade = 10;
pessoa['idade']; // 10

    Aqui começa a aparecer algumas limitações. Na notação literal apenas é possível informar o value (logicamente) ou o get e set, todos aquelas outras características dos descritores são tidas como true:

pessoa.cpf = '88733172943';
// É equivalente a
Object.defineProperty(pessoa, 'cpf', {
  value: '88733172943',
  writable: true,
  configurable: true,
  enumerable: true
});
// Enquanto
Object.defineProperty(pessoa, 'cpf', { value: '88733172943' });
// É equivalente a, como dito na seção de descritores
Object.defineProperty(pessoa, 'cpf', {
  value: '88733172943',
  writable: false,
  configurable: false,
  enumerable: false
});

    Mas claro que somos livres para utilizar ambas as notações citadas:

var pessoa = {
  nome: 'Jaon',
  sobrenome: '0blivion'
};

Object.defineProperty(pessoa, 'idade', { value: 50
                                       , writable: false
                                       });
pessoa.idade; //50
pessoa.idade = 10;
pessoa['idade']; //50

    Na última linha acessamos a proprieddae idade de um jeito diferente. Pois é, podemos acessar propriedades tanto utilizando a dot-notation, quanto a bracket-notation. Fica a dica.

##6. Herança e protótipos     A herança em Javascript é baseada em protótipos. Protótipos são apenas objetos que compartilham seus membros (propriedades e métodos) com outros objetos. A herança em Javascript é simploria, ocorre através de delegação. Você pode ligar um objeto a outro de forma que o primeiro não herde, mas tenha acesso aos membros do último objeto.

    Então, ao herdar, você não estará criando uma cópia dos membros de algum objeto e atribuindo a um novo, todas eles ficam no próprio parente e seu acesso é apenas extendido ao filho. Quando uma propriedade que não existe de um objeto é acessada, essa propriedade é procurada no parente do objeto, se não tiver lá, procura-se no parente desse parente e assim segue até chegar à raiz. Por isso herança múltiplas não é suportada aqui, os objetos se ligam em forma de corrente.

var Wallet = {
  money: 0,
  getGoodNews: function() {
    console.log('Ok, there is ' + this.money + ' dollars.');
  }
};

var myPocket = Object.create(Wallet); // Cria um novo objeto herdando de Wallet

myPocket.getGoodNews(); // "Ok, there is 0 dollars."

//Alterar o protótipo
Wallet.getGoodNews = function() {
  console.log('I could give you ' + this.money + ' good news.');
};
//Afetará os objetos ligados a ele
myPocket.getGoodNews(); // "I could give you 0 good news."

//A menos que o 'polimorfismo' já tenha entrado em ação
myPocket.money; // 0
Wallet.money = 20;
myPocket.money; // 20
myPocket.money = 50;
Wallet.money; // 20
myPocket.money; // 50

myPocket.getGoodNews(); // "I could give you 50 good news."

    Para saber se um objeto é protótipo de outro, dispomos dos métodos isPrototypeOf e getPrototypeOf. Lembre-se que protótipo é o objeto que tem seus membros compartilhados, não o que tem acesso a eles:

Wallet.isPrototypeOf(myPocket); // true
myPocket.isPrototypeOf(Wallet); // false
Object.getPrototypeOf(myPocket) === Wallet; // true
Object.getPrototypeOf(Wallet) === myPocket; // false

    Agora vejamos alguns casos de inicialização. Aqui evitarei palavras, vou apenas plantar a semente, então peço que preste muita atenção nos exemplos desta seção.

var Person = {
  meet: function() {
    console.log('Hi, my name is ' + this.name);
  };
};

var Jaon = Object.create(Person, {
  name: {
    value: 'ja0n',
    writable: false,
  }
});

Jaon.name; // "ja0n"
Jaon.name = 'Tyler';
Jaon.name; // "ja0n"

Jaon.meet(); // "Hi, my name is ja0n"

    Aqui fizemos uso do segundo parâmetro do método Object.create que falei lá em cima. Agora vejamos alguns exemplos onde implementamos funções de inicialização do objeto:

var Person = {
  init: function(name) {
    this.name = name;
  },
  meet: function() {
    console.log('Hi, my name is ' + this.name);
  };
};

var Jaon = Object.create(Person);
Jaon.init('ja0n');
Jaon.meet(); // "Hi, my name is ja0n"

//Eu particularmente prefiro do jeito a seguir, que utiliza uma técnica chamada method-chaining...
var Person = {
  init: function(name) {
    this.name = name;
    return this;
  },
  meet: function() {
    console.log('Hi, my name is ' + this.name);
  };
};

var Jaon = Object.create(Person).init('ja0n'); // Assunto pra outra vida :P
Jaon.meet(); // "Hi, my name is ja0n"

##7. Construtores e o new

Melhorar isso aqui depois

    Toda função tem uma propriedade chamada prototype. Todas as instâncias criadas a partir de uma função herdarão os membros de seu prototype. Assim, se quisermos adicionar um método ao empacotador Number, podemos fazer alterando seu prototype:

Number.prototype.plusTwo = function() { return this + 2 };

var five = 5, three = 3;

three.plusTwo(); //5
five.plusTwo(); //7

    E é isso. Agora vamos fazer nosso próprio construtor:

var Person = function(name, age) {
  this.name = name;
  this.age = age;
}

//Vamos alterar seu prototype
//para afetar suas futuras instâncias
Person.prototype.planet = "Earth";

Person.prototype.greet = function() {
  console.log('Hello, my name is ' + this.name + ', I am ' + this.age + ' years old and I live on ' + this.planet);
}

var Jaon = new Person('Jaon 0blivion', 50);
Jaon.planet; // "Earth"
Jaon.greet(); // "Hello, my name is Jaon 0blivion, I am 50 years old and I live on Earth"

    Pronto, agora perceba que para a mágica acontecer precisa da keyword new. Entenda o que ela faz:

  • Cria um novo objeto como {}
  • Liga o objeto recém-criado ao prototype da função, assim herdando tudo que tiver lá
  • Chama a função usando o objeto recém-criado como contexto
  • Se a função retorna um objeto, retorna essa objeto
  • Senão, retorna o objeto recém-criado

    Agora vamos ver como aplicar herança nos contrutores:

var Marcian = function(name, age) {

  Person.call(this, name, age);
};

Marcian.prototype = Object.create(Person.prototype);
Marcian.prototype.planet = 'Mars';
Marcian.prototype.constructor = Marcian; //depois eu explico essa linha


var ET = new Marcian('Tyler', 450);
ET.planet; // "Mars"
ET.greet(); // "Hello, my name is Tyler, I am 450 years old and I live on Mars"

##8. Considerações     Bom, até aqui espero ter pelo menos despertado a curiosidade do leitor. Eu entendo que por muito tempo um aprendizado mais concreto em Javascript foi descartado por ser uma linguagem que só servia pra fazer algumas coisinhas aqui e ali no browser, que decorar as funções do jQuery era muito mais produtivo e etc. Mas hoje em dia, com o advento do Javascript server-side trazido pelo NodeJS, acredito que seja a hora e a vez disso acontecer. Javascript foi muito mal encompreendida por apresentar aspectos diferentes, mas temos que abrir nossa mente para ver o quão benéfico esses aspectos podem ser. Enfim, peço que confiem em mim: Quebrar a cabeça para entender uma abordagem de OO diferente em um cenário deturpado por classes vale a pena, principalmente por causa do promissor NodeJS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment