Olá, pessoal! No artigo de hoje vamos falar da ECMAScript 5, que está virando realidade, tanto no front-end com os grandes navegadores, quanto no back-end com Node.js e outros que utilizam o motor V8 – muitas das coisas que fazemos hoje poderão ser feitas muito mais facilmente ou, pelo menos, de forma mais confiável. Confiança é, de fato, a palavra chave aqui, e na parte de orientação a objetos a ECMAScript 5 está dando um show!
A nova API de Object permite que façamos coisas que até então eram impossíveis; proteger a interface de um objeto é uma delas. Vamos falar sobre essa nova API de Object e ver algumas novas possibilidades.
Object
Como todos sabem, em Javascript praticamente tudo é um Object. String, Number, Array, Function e a grande maioria das coisas em Javascript herda a interface de Object, isso significa que:
> String instanceof Object;
true
> Array instanceof Object;
true
> Number instanceof Object;
true
> Function instanceof Object;
true
No Javascript, essa herança é definida por um padrão de criação chamado Prototype, ou seja, a partir da instância de um protótipo, criamos novos objetos copiando esse protótipo:
> function Collection(){}
undefined
> Collection.prototype = new Array();
[]
> var c = new Collection();
undefined
> c.push(1);
1
> c.push(2);
2
> c.push(3);
3
> c;
{ '0': 1,
'1': 2,
'2': 3,
length: 3 }
> c instanceof Collection;
true
> c instanceof Array;
true
>
Como podemos ver, a criação de objetos que são de determinado tipo, é muito simples com Javascript. O problema mesmo se dá quando apagamos ou modificamos coisas de nossos objetos na ECMAScript 3, vejam só:
> var sampa = {
... js: {
..... agenda: [{
....... s:new Date(2012,02,17,13),
....... e:new Date(2012,02,17,13,50),
....... name:"João Batista Neto"
....... }],
..... current: function() {
....... var n = new Date();
....... for (var i=0;i<this.agenda.length;++i){
......... if (n>=this.agenda[i].s&&n<=this.agenda[i].e){
........... return this.agenda[i];
........... }
......... }
....... return {s:n,e:n,name:null};
....... }
..... }
... };
undefined
> sampa.js.current().name;
'João Batista Neto'
Se no dia do evento sampa.js, o código acima tivesse sido executado entre 13h e 13h50, a saída João Batista Neto estava de acordo com o esperado por qualquer client que tivesse visto a agenda do evento e tivesse ido assistir as palestras. Mas, na ECMAScript 3, tudo aquilo que podemos ver, podemos também deletar:
delete sampa.js.current;
Se nossos clientes continuarem confiando na interface desse objeto, teremos um problema quando tentarem utilizar o método current:
> sampa.js.current().name;
TypeError: Object #<Object> has no method 'current'
at repl:1:10
at REPLServer.eval (repl.js:80:21)
at repl.js:190:20
at REPLServer.eval (repl.js:87:5)
at Interface.<anonymous> (repl.js:182:12)
at Interface.emit (events.js:67:17)
at Interface._onLine (readline.js:162:10)
at Interface._line (readline.js:426:8)
at Interface._ttyWrite (readline.js:603:14)
at ReadStream.<anonymous> (readline.js:82:12)
>
Ou seja, nossos clients precisam testar a existência desse método e, se precisamos testar, começamos a perder a confiança nas coisas:
> if (typeof sampa.js.current=="function"){
... sampa.js.current().name; //'João Batista Neto'
... }
As coisas ficam ainda piores pois, se podemos ver, podemos também modificar, e aqui moram os bugs mais difíceis de se depurar:
> sampa.js.current = function(){
... return {
..... s:new Date(2012,02,17,13),
..... e:new Date(2012,02,17,19,50),
..... name:"Michel Teló - Full concert"
..... };
... };
[Function]
> if (typeof sampa.js.current=="function"){
... sampa.js.current().name;
... }
'Michel Teló - Full concert'
Ou seja, testamos a existência de current, testamos ainda se current é, de fato, um método. Mas como ele foi modificado, temos um problema sério, mais sério ainda do que a não existência do método; um problema difícil de se testar, validar e, principalmente, confiar.
Para resolvê-lo, descritores de propriedades foram adicionados à ECMAScript 5 para que possamos especificar nossas propriedades e, assim, permitir ou não que alguma coisa aconteça.
> var descriptor = {
... writable: true || false,
... enumerable: true || false,
... configurable: true || false,
... value: null /*qualquer tipo de valor*/,
... get: function(){},
... set: function(v){}
. };
- writable – Indica se podemos ou não escrever nessa propriedade. Se definido como false, o problema de modificação de um método para uma String ou qualquer outra coisa é resolvido.
- enumerable – Quando iteramos um objeto com for (var prop in Object ), prop receberá os nomes das propriedades de Object. Porém, é comum termos propriedades que são estruturas de dados específicas de determinado algorítimo; estruturas essas que não devem ser expostas. Configurando enumerable como false, essa propriedade não será listada em uma iteração como a ilustrada.
- configurable – Mesmo que tenhamos indicado que uma propriedade não é gravável e não é enumerável, ela ainda pode ser configurada para ser o contrário. Se pegarmos uma propriedade qualquer e seu descritor estiver configurado com configurable: true, então podemos pegar e definir que writable, que era false, agora é true também, redefinindo essa configuração. Então, configurable define a capacidade de configurar ou não uma determinada propriedade.
- get e set – Esses dois métodos especiais são para permitir o acesso e definição do valor de uma determinada propriedade. Eles são especialmente interessantes, pois podemos ter getters inteligentes e setters restritivos, ou seja, podemos ter um meio de acesso simples à determinado valor, e restringir apenas determinados tipos em determinadas propriedades. Vamos ver mais sobre esses dois caras mais abaixo.
Object.defineProperty(object,property,descriptor);
Com Object.defineProperty vamos começar a utilizar os descritores que vimos acima, por exemplo:
> var Square = {};
undefined
> Object.defineProperty(Square,"width",{
... writable: true,
... configurable: false,
... enumerable: true,
... value: null
... });
{ width: null }
> Object.defineProperty(Square,"height",{
... writable: true,
... configurable: false,
... enumerable: true,
... value: null
... });
{ width: null, height: null }
Como podem ver, criamos um objeto Square e definimos duas propriedades: width e height. Como configuramos Square.width e Square.height como writable, podemos então definir seus valores:
> Square.width = 10;
10
> Square.height = 10;
10
Como definimos Square.width e Square.height como enumerable, podemos então iterá-los:
> for (var prop in Square) console.log(prop);
width
height
Mas, configurable foi definida como false, o que isso significa para o nosso código? Significa que, além de não poder redefinir seu descritor, também não podemos deletar a propriedade:
> delete Square.width;
false
> Square.width;
10
As coisas ficam mais legais ainda quando definimos um getter, vejam só:
> Object.defineProperty(Square,"area",{
... get: function(){ return this.width * this.height; }
... } );
{ width: 10, height: 10 }
> Square.area;
100
Object.defineProperties(object,descriptors)
A diferença entre Object.defineProperty e Object.defineProperties está no número de propriedade que podemos definir com cada um dos dois. Enquanto a definição das propriedades é feita uma a uma com Object.defineProperty, Object.defineProperties permite a definição de várias propriedades ao mesmo tempo:
> Object.defineProperties(Square, {
... width: {
..... writable: true, enumerable: true, configurable: false,
..... value: null
..... },
... height: {
..... writable: true, enumerable: true, configurable: false,
..... value: null
..... },
... area: {
..... get: function(){ return this.width * this.height; }
..... }
... });
{ width: null, height: null }
> Square.width = Square.height = 10;
10
> Square.area;
100
Object.getOwnPropertyDescriptor(object,property)
Diretamente relacionado com Object.defineProperty, o Object.defineProperties e a propriedade configurable do descritor, o método Object.getOwnPropertyDescriptor(), nos retorna o descritor de uma propriedade, por exemplo:
> var Square = {};
> Object.defineProperty(Square,"width",{
... writable: true,configurable: true, enumerable: true,
... value: null
... });
{ width: null }
> Square.width = 10;
10
> Object.getOwnPropertyDescriptor(Square,"width");
{ value: 10,
writable: true,
enumerable: true,
configurable: true }
Isso é especialmente interessante se precisarmos fazer alguma modificação, vejamos:
> var descriptor = Object.getOwnPropertyDescriptor(Square,"width");
> descriptor;
{ value: 10,
writable: true,
enumerable: true,
configurable: true }
> if (descriptor.configurable){
... descriptor.configurable=false;
... descriptor.enumerable=false;
... Object.defineProperty(Square,"width",descriptor);
... }
{}
> Object.getOwnPropertyDescriptor(Square,"width");
{ value: 10,
writable: true,
enumerable: false,
configurable: false }
Object.keys() e Object.getOwnPropertyNames()
Esses dois métodos permitirão que recuperemos um Array com os nomes das propriedades de um objeto – a única diferença entre os dois está no tipo do retorno. Enquanto o Object.keys() retorna apenas as propriedades enumeráveis, o Object.getOwnPropertyNames() vai retornar tudo, inclusive as não enumeráveis.
> var Objeto = {};
undefined
> Object.defineProperties(Objeto,{
... enumeravel: {enumerable: true, value: 1},
... nao_enumeravel: {enumerable: false, value: 2}
... });
{ enumeravel: 1 }
> Object.keys(Objeto);
[ 'enumeravel' ]
> Object.getOwnPropertyNames(Objeto);
[ 'enumeravel', 'nao_enumeravel' ]
Como podem ver, somente a propriedade “enumeravel” é retornada com Object.keys, enquanto além da “enumeravel”, a “nao_enumeravel” é retornada também com Object.getOwnPropertyNames().
Nota pessoal: Acho que o método Object.getOwnPropertyNames() viola o que foi configurado no descritor da propriedade, quando definimos enumerable como false e, por esse motivo, esse método não deveria existir. Se temos a possibilidade de configurar uma propriedade como não enumerável, não deveria haver um método que facilite sua enumeração.
Object.create(prototype,descriptors)
Object.defineProperty() e Object.defineProperties() são muito úteis quando já temos um objeto que queremos configurar. Agora, em novos objetos podemos utilizar o método Object.create() para criar um novo objeto, configurar suas propriedades e, ainda, definir qual o tipo desse objeto, por exemplo:
> var Shape = Object.create(Object.prototype,{
... width: {
..... writable: true, enumerable: true, configurable: false,
..... value: 0
..... },
... height: {
..... writable: true, enumerable: true, configurable: false,
..... value: 0
..... },
... area: {
..... get: function(){ return this.width * this.height; }
..... }
... });
> Shape.width = Shape.height = 10;
10
> Shape.area;
100
Object.seal(object) e Object.isSealed(object)
Após a criação de um novo objeto, podemos protegê-lo de modificações, ou seja, definir configurable como false, mas de uma forma mais simples, sem precisar modificar os descritores de cada propriedade. Para isso, basta que utilizemos Object.seal(), por exemplo:
> var Shape = {};
> Shape.width = 10;
10
> delete Shape.width;
true
> Shape.width;
undefined
> var Shape = {};
> Shape.width = 10;
10
> Object.seal(Shape);
{ width: 10 }
> delete Shape.width;
false
> Shape.width
10
Se observarmos o descritor da propriedade width antes e depois de utilizar o método Object.seal(), teremos o seguinte:
> var Shape = {};
> Shape.width = 10;
10
> Object.getOwnPropertyDescriptor(Shape,"width");
{ value: 10,
writable: true,
enumerable: true,
configurable: true }
Antes do Object.seal(), configurable está definido como true, que é o valor padrão, agora com o Object.seal():
> Object.seal(Shape);
{ width: 10 }
> Object.getOwnPropertyDescriptor(Shape,"width");
{ value: 10,
writable: true,
enumerable: true,
configurable: false }
Ou seja, apenas modificamos configurable para false. Object.isSealed() é auto-explicativo, ele apenas nos informa se o objeto está “selado”, por exemplo:
> var Shape = {};
> Shape.width = 10;
10
> Object.isSealed(Shape);
false
> Object.seal(Shape);
{ width: 10 }
> Object.isSealed(Shape);
true
Object.freeze(object) e Object.isFrozen(object)
Semelhante ao Object.seal() e ao Object.isSealed(), o Object.freeze() e o Object.isFrozen() modificam o descritor da nossa propriedade, mas dessa vez, o writable também é definido como false, ou seja além de não poder modificar o descritor ou deletar a propriedade, não poderemos modificar seu valor:
> var Shape = {};
> Shape.width = 10;
10
> Object.freeze(Shape);
{ width: 10 }
> Shape.width = "um outro valor";
'um outro valor'
> Shape.width;
10
> delete Shape.width;
false
> Shape.width;
10
Novamente, observando os descritores com Object.getOwnPropertyDescriptor() teremos o seguinte:
> var Shape = {};
> Shape.width = 10;
10
> Object.getOwnPropertyDescriptor(Shape,"width");
{ value: 10,
writable: true,
enumerable: true,
configurable: true }
> Object.freeze(Shape);
{ width: 10 }
> Object.getOwnPropertyDescriptor(Shape,"width");
{ value: 10,
writable: false,
enumerable: true,
configurable: false }
Como podem ver, antes configurable e writable estavam definidos como true, que é o valor padrão. Após Object.freeze(), tanto configurable quanto writable passaram a ser false, ou seja, não podemos deletar, reconfigurar nem modificar seus valores. Obviamente, da mesma forma que Object.isSealed(), Object.isFrozen() verificará se determinado objeto está “congelado”:
> var Shape = {};
> Shape.width = 10;
10
> Object.isFrozen(Shape);
false
> Object.freeze(Shape);
{ width: 10 }
> Object.isFrozen(Shape);
true
Como podemos ver, a ECMAScript 5 trouxe muita coisa bacana em relação aos objetos. Além dessa nova API de Object, muitas outras coisas foram adicionadas, como uma melhoria na API de Arrays e Function.bind nativo. No próximo artigo, vou mostrar um pouco das possibilidades da nova API de Arrays e como ela pode ajudar a fazer, muito facilmente, coisas que eram um tanto complexas antes.
Para quem não foi no SampaJS, estou deixando o vídeo da palestra que discuti ECMAScript 5 abaixo. Quero deixar o link do post da Laura Loenert, que gentilmente permitiu que utilizasse esse vídeo nesse artigo. Obrigado Laura!