#(Quase) Tudo que você precisa saber sobre objetos em Javascript
Roteiro
- Javascript é sobre objetos
- Declaração
- Propriedades e descritores
- Métodos e o this
- Notação literal
- Herança e protótipos
- Construtores e o new
- 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 ovalue
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.