JavaScript Garden é uma coletânea crescente que documenta as peculiaridades da linguagem de programação JavaScript. Nela você encontra recomendações para escapar dos erros comuns aos mais sutís, bem como de problemas de desempenho e práticas ruins, que programadores novatos podem acabar encontrando enquanto se aprofundam na linguagem.
JavaScript Garden não tem como propósito te ensinar à programar em JavaScript. Conhecimento prévio da linguagem é fortemente recomendado para que você entenda o conteúdo dos tópicos abordados neste guia. A fim de aprender as noções básicas da linguagem, por favor, consulte o excelente guia disponível na Mozilla Developer Network.
JavaScript Garden está publicado sob a licença MIT e hospedado no GitHub. Caso você encontre defeitos ou erros de digitação, por favor registre o problema ou realize um pull request no repositório. Você também pode nos encontrar na sala de JavaScript no chat do Stack Overflow.
Objetos
Propriedades e manipulação de objetos
Tudo em JavaScript se comporta como um objeto, com apenas duas exceções que são
null e undefined.
Um equívoco muito comum é a idéia de que números não podem ser manipulados como objetos. O parser do JavaScript analisa a notação de ponto como ponto flutuante de um número.
2.toString(); // raises SyntaxError
Existem três soluções para contornar este problema e permtir que números se comportem como objetos.
2..toString(); // o segundo ponto é reconhecido corretamente
2 .toString(); // perceba o espaço deixado à esquerda do ponto
(2).toString(); // 2 é interpretado primeiro
Objetos como tipo de dados
Em JavaScript, Objetos podem também ser utilizados como Hashmaps; eles consistem principalmente de propriedades nomeadas, que apontam para valores.
Usando um objeto literal - notação do tipo {}- é possível criar um objeto simples. Este novo objeto herda de Object.prototype e não possui propriedades próprias definidas.
var foo = {}; // um novo objeto vazio
// um novo objeto com uma propriedade 'test' populada com o valor 12
var bar = {test: 12};
Acessando propriedades
As propriedades de um objeto podem ser acessadas de duas maneiras, através da notação de ponto ou da notação de colchete.
var foo = {name: 'kitten'}
foo.name; // kitten
foo['name']; // kitten
var get = 'name';
foo[get]; // kitten
foo.1234; // Erro de sintaxe
foo['1234']; // funciona
Ambas as notações trabalham de forma quase idêntica, com a única diferença de que o colchete permite configuração dinâmica de propriedades e uso de propriedades nomeadas que de outra maneira levaria à erros de sintaxe.
Removendo propriedades
A única maneira de remover uma propriedade de um objeto é através do operador delete; definir uma propriedade como undefined ou null somente apaga o valor associado com tal propriedade, mas não remove a key.
var obj = {
bar: 1,
foo: 2,
baz: 3
};
obj.bar = undefined;
obj.foo = null;
delete obj.baz;
for(var i in obj) {
if (obj.hasOwnProperty(i)) {
console.log(i, '' + obj[i]);
}
}
O código acima retorna tanto bar undefined quantofoo null - somente baz foi removido e, portanto, não saiu no output.
Notações de Keys
var test = {
'case': 'I am a keyword, so I must be notated as a string',
delete: 'I am a keyword, so me too' // dispara SyntaxError
};
Propriedades de objetos podem ser tanto representadas como caracteres simples bem como strings. Devido a outro engano do parser do JavaScript, o exemplo acima irá retornar SyntaxError remetendo ao ECMAScript 5.
Este erro decorre do fato de que 'apagar' é uma palavra reservada; por consequencia, deve ser representada como uma string literal a fim de garantir a interpretação correta pelas antigas engines de JavaScript.
Prototype
JavaScript não dispõe de nenhum modelo clássico de herança; em vez disso, ele
faz uso do modelo prototypal.
Enquanto isto é considerado muitas vezes como sendo um dos pontos fracos do JavaScript, o modelo de herança prototypal é de fato muito mais poderoso do que o modelo clássico.
Por exemplo, isto torna relativamente trivial construir um modelo clássico
com base no modelo prototypal, enquanto que o contrário se verifica como uma tarefa mais difícil.
JavaScript é a única linguagem amplamente utilizada que apresenta um modelo de herança do tipo prototypal,
por isso pode levar algum tempo até que você se ajuste às diferenças entre os dois modelos.
A primeira grande diferença é que herança em JavaScript utiliza o conceito de cadeias prototype.
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// Apontar Bar's prototype para uma nava instância de Foo
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// Tenha certeza de que Bar é o construtor atual
Bar.prototype.constructor = Bar;
var test = new Bar(); // criar uma nova instância de bar
// A cadeia prototype resultante
test [instance of Bar]
Bar.prototype [instance of Foo]
{ foo: 'Hello World', value: 42 }
Foo.prototype
{ method: ... }
Object.prototype
{ toString: ... /* etc. */ }
No código acima, o objeto test irá herdar de ambos Bar.prototype e
Foo.prototype; portanto, ele terá acesso à função method que foi definida em Foo.
Ele também terá acesso à propriedade value da única instância de Foo que é seu próprio prototype.
É importante perceber que new Bar() não cria uma nova instância de Foo, mas
reutiliza aquela associada ao prototype; assim, todas as intâncias Bar dividirão a
mesma propriedade value.
Buscando propriedades
Ao acessar as propriedades de um objeto, JavaScript irá percorre a cadeia prototype
até o topo para encontrar a propriedade solicitada.
Caso atinja o topo da cadeia - denominada Object.prototype - e não encontre
a propriedade especificada, o valor undefined será retornado.
A propriedade Prototype
Enquanto a propriedade prototype é utilizada pela linguagem na contrução de cadeia de prototype,
ainda é possível associar qualquer valor dado a ele. No entanto, tipos primitivos serão
ignorados quando associados como prototype.
function Foo() {}
Foo.prototype = 1; // sem efeito
Atribuindo objetos, como demonstrado no exemplo anterior, irá funcionar, e permite
a criação dinâmica de cadeias prototype.
Performance
O tempo de pesquisa por propriedades que estão no topo da cadeia prototype
pode ter um impacto negativo na performance, principalmente em código
onde a performance é um fator crítico. Além disso, a busca por propriedades que não existem
também atravessa a cadeia prototype.
Além disso, ao interagir com propriedades de um objeto
cada propriedade na cadeia prototype será enumerada.
Estendendo Prototypes nativos
Uma prática ruim que é normalmente utilizada é a de estender Object.prototype ou qualquer outro prototype construído.
Esta técnica é denominada monkey patching e quebra o encapsulamento.
Mesmo utilizada por frameworks populars como Prototype, não existe mais razão
para poluir tipos built-in com funcionalidades adicionais fora de padrão.
A única boa razão existente para continuar estendendo um built-in prototype
é a de assegurar as novas funcionalidade de engines JavaScript modernas; por exemplo, Array.forEach.
Conclusão
É essencial entender o modelo de herança prototypal antes de escrever código complexo
que faço uso do mesmo. Além disso, tome cuidado com o tamanho da cadeia prototype em seu código
e a refatore caso necessário a fim de evitar futuros problemas de performance. A respeito do prototypes nativos,
estes nunca devem ser estendidos ao menos que seja para manter a compatibilidade com novas
características do JavaScript.
hasOwnProperty
Para verificar se uma propriedade está definida no próprio objeto e não em outro lugar
da sua cadeia prototype, é necessário utilizar o método
hasOwnProperty o qual todos os objetos herdam de Object.prototype.
hasOwnProperty é a única coisa em JavaScript a qual lida com propriedades e não percorre a cadeia prototype.
Somente hasOwnProperty irá retornar o resultado correto e esperado; isto é
essencial quando se interage sobre propriedades de qualquer objeto. Não existe
outra maneira de verificar propriedades que não estejam definidas no próprio objeto, mas
em outro lugar na cadeia prototype.
hasOwnProperty como propriedade
JavaScript não protege o nome da propriedade hasOwnProperty; assim, se
existe a possibilidade de algum objeto possuir uma propriedade com este mesmo nome,
torna-se necessário utilizar um hasOwnPropertyexterno a fim de obter resultados corretos.
var foo = {
hasOwnProperty: function() {
return false;
},
bar: 'Here be dragons'
};
foo.hasOwnProperty('bar'); // sempre retorna false
// Utiliza hasOwnProperty de outro objeto e o instancia com 'this' apontado para foo
({}).hasOwnProperty.call(foo, 'bar'); // true
// Também é possível utilizar hasOwnProperty do Object
// prototype para este fim
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true
Conclusão
O método hasOwnProperty é a única maneira confiável para verificar a existência da propriedade em um objeto.
É recomendado que hasOwnProperty seja utilizado em cada interação de um laço for in
a fim de evitar erros de extensão do prototype.
O laço for in
Assim como o operador in, o laço for in percorre a cadeia prototype quando interage sobre as propriedades de um objeto.
// Poluindo o Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // retorna ambos bar e moo
}
Uma vez que não é possível alterar o comportamento do laço for in por si só, faz-se necessário filtrar as propriedades do objeto durante o ciclo de repetição do laço; isso é feito usando o método hasOwnProperty do Object.prototype.
Utilizando hasOwnProperty como filtro
// o mesmo foo utilizado anteriormente
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i);
}
}
Esta é única forma correta de usar. Devido ao uso de hasOwnProperty, o exemplo só irá retornar moo. Quando hasOwnProperty é deixado de lado, o código fica propenso a erros nos casos em que o prototype - por exemplo Object.prototype- tenha sido estendido.
Um framework largamente utilizado que estende o Object.prototype é Prototype.
Quando este framework é utilizado, laços for in que não utilizam
hasOwnProperty ficam desprotegidos contra erros.
Conclusão
Recomenda-se utilizar hasOwnPropertysempre. Nunca faça pressuposições sobre o ambiente em que o código está sendo executado, ou se os prototypes nativos foram estendidos ou não.
Funções
Declaração de funções e expressões
Funções em JavaScript são objetos de primeira classe. Isto significa que elas
podem ser tratadas como qualquer outro tipo. Um uso muito comum desta característica é
o de passar uma função anônima como uma callback para outra, talvez uma função assíncrona.
A declaração function
function foo() {}
A função acima sofrerá hoisting antes que a execução do programa se inicie; assim,
ela estará disponível em todo o escopo em que foi definida, até mesmo se for chamada antes de ter
sido definida no código.
foo(); // Funciona pois foo foi elevada para o topo do escopo
function foo() {}
A expressão function
var foo = function() {};
Este exemplo atribui uma função sem nome e anônima à variável foo.
Devido ao fato de que var é uma declaração que eleva a definição da variável foo ao topo do escopo, esta sofrerá hoist e foo estará declarado logo que o script for executado.
Como atribuições só ocorrem em tempo de execução, o valor default de foo
será undefined antes que o código seja executado.
Expressão de uma função nomeada
Outro caso especial é a atribuição de funções nomeadas.
var foo = function bar() {
bar(); // Funciona
}
bar(); // ReferenceError
Aqui, bar não está disponível fora do escopo, uma vez que a função se encontra atribuída
em foo; no entanto, dentro de bar, ela esta disponível. Isto ocorre devido ao fato de
como o name resolution funciona em JavaScript, o nome da função está sempre
disponível no escopo local da própria função.
Como funciona o this
JavaScript tem uma concepção diferente sobre a que a palavra reservada this se refere da maioria das outras linguagens de programação. Existem exatamente cinco diferentes maneiras as quais os valores de this podem ser referenciados na linguagem.
O escopo Global
this;
Quando usando this no escopo global, ele simplesmente estará apontando para o objeto global.
Chamando uma função
foo();
Aqui, this irá referenciar novamente o objeto global.
Chamando um método
test.foo();
Neste exemplo, this irá referenciar test.
Chamando um construtor
new foo();
Uma chamada de função que é precedida pela palavra chave new age como
um construtor. Dentro da função, this irá se referir
a um objeto recém criado.
Referência explícita do this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // array will expand to the below
foo.call(bar, 1, 2, 3); // results in a = 1, b = 2, c = 3
Quando utiliza-se os métodos call ou apply de Function.prototype, o valor de
this dentro da função chamada irá referenciar explicitamente o primeiro argumento
correspondente na chamada da função.
Como resultado, no exemplo anterior o escopo original do métodonão é aplicado, e this
dentro de foo irá referenciar bar.
Erros comuns
Embora a maioria destes casos façam sentido, o primeiro pode ser considerado
como um engano de concepção da linguagem, já que nunca se mostrou útil.
Foo.method = function() {
function test() {
// this referencia o objeto global
}
test();
};
Um erro comum é achar que this dentro de test referencia Foo; enquanto que, na realidade
não é isto que acontece.
Com a finalidade de acessar Foo de dentro de test, é necessário instanciar
uma variável global dentro do método para se referir à Foo.
Foo.method = function() {
var that = this;
function test() {
// Utilize that no lugar de this aqui
}
test();
};
that trata-se de uma variável normal, porém é normalmente utilizada para referências externas de this.
Quando combinadas com closures, também podem ser utilizadas para repassar this como valor.
Atribuindo métodos
Outra coisa que não funciona em JavaScript é function aliasing, ou seja,
atribuir um método a uma variável.
var test = someObject.methodTest;
test();
Devido ao primeiro caso, test se comportará como uma chamada de função; como consequencia,
o this dentro do método não apontará para someObject.
Enquanto que realizar binding do this pareça uma idéia ruim, no fundo, é o que faz a
herança prototypal funcionar.
function Foo() {}
Foo.prototype.method = function() {};
function Bar() {}
Bar.prototype = Foo.prototype;
new Bar().method();
Quando os métodos da instância de Bar são chamados, o this faz referência
àquela mesma instância.
Closures e Referências
Uma das caracterísricas mais poderosas do JavaScript é a possibilidade de usar closures. Quando usamos closures, definimos que um escopo sempre poderá acessar o escopo externo no qual foi definido. Uma vez que a única concepção de escopo em JavaScripe é function scope, todas as funções, por default, agem como closures.
Emulando variáveis privadas
function Counter(start) {
var count = start;
return {
increment: function() {
count++;
},
get: function() {
return count;
}
}
}
var foo = Counter(4);
foo.increment();
foo.get(); // 5
Aqui, Counter retorna duas closures: a função 'increment' bem como a função 'get'. Ambas as funções mantêm uma referência ao escopo de 'Counter' e, portanto, sempre mantêm o acesso à variável 'count' definida naquele escopo.
Por que variáveis privadas funcionam
Uma vez que não é possível referenciar ou atribuir escopos em JavaScript, não existe uma maneira de acessar a variável 'count' por fora. A única maneira de interagir com a variável é através das duas closures.
var foo = new Counter(4);
foo.hack = function() {
count = 1337;
};
O código acima não irá mudar a variável 'count' no escopo de 'Counter', uma vez que 'foo.hack' não foi definido naquele escopo. Neste caso, uma variável 'global' 'count' será criada ou substituida.
Closures dentro de laços
Um erro comum é utilizar closures dentro de laços, como se elas copiassem o valor da variável de indexação do laço.
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
O exemplo acima não retornará os números '0' até '9', mas os número '10' dez vezes.
A função anônima mantêm uma referência para 'i'. No momento em que 'console.log' é chamado, 'o laço for' já encerrou a execução, e o valor '10' está atrbuído em 'i'.
Com a finalidade de se obter o comportamento esperado, é necessário criar uma cópia do valor de 'i'.
Evitando problemas de referência
Com a finalidade de copiar o valor da variável de indexação do laço, a melhor opção é utilizar um wrapper anônimo.
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}
A função anônima será chamada imediatamente com 'i' como seu primeiro argumento e receberá uma cópia do valor de 'i' como parâmetro de 'e'.
A função anônima que é passada para o 'setTimeout' agora possui uma referência a 'e', cujo os valores não são modificados pelo laço.
Não existe outra maneira de se obter este resultado, que não seja retornando uma função do wrapper anônimo que terá, então, o mesmo comportamento que o código acima.
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}
Há ainda uma outra maneira, usando .bind, que pode ligar argumentos e um contexto'this' a uma função. Isto se comporta como o código acima
for(var i = 0; i < 10; i++) {
setTimeout(console.log.bind(console, i), 1000);
}
O objeto arguments
Todo escopo de uma função em JavaScript tem acesso à variável especial arguments.
Esta variável armazena uma lista de todos os argumentos que foram passados para a função.
O objeto argumentsnão é um Array. Enquanto que ele possui uma semântica
parecida com a de um array - a saber a propriedade length - ele não herda de Array.prototype
e é de fato um Object.
Devido a isto, não é possível usar os métodos padrões de array como push,
pop ou slice no arguments. Enquanto que a iteração com um simples for loop funciona bem,
é necessário convertê-lo para um Array a fim de usar os métodos padrões de Array.
Convertendo em um Array
O código abaixo irá retornar um novo Array contendo todos os elementos do
objeto arguments.
Array.prototype.slice.call(arguments);
Por este tipo de conversão ser lenta, seu uso em porções de código que apresentam performance crítica não é recomendado.
Passando argumentos
O código abaixo é a maneira recomendada de se passar argumentos de uma função para outra.
function foo() {
bar.apply(null, arguments);
}
function bar(a, b, c) {
// do stuff here
}
Outro truque é o de usar ambos call e apply juntos para criar wrappers.
function Foo() {}
Foo.prototype.method = function(a, b, c) {
console.log(this, a, b, c);
};
// Create an unbound version of "method"
// It takes the parameters: this, arg1, arg2...argN
Foo.method = function() {
// Result: Foo.prototype.method.call(this, arg1, arg2... argN)
Function.call.apply(Foo.prototype.method, arguments);
};
Parâmetros formais Formal Parameters and Arguments Indices
O objeto arguments cria funções getter e setter para suas propriedades,
bem como os parâmetros formais da função.
Como resultado, alterando o valor de um parâmetro formal também mudará o valor
da propriedade correspondente no objeto arguments, e vice versa.
function foo(a, b, c) {
arguments[0] = 2;
a; // 2
b = 4;
arguments[1]; // 4
var d = c;
d = 9;
c; // 3
}
foo(1, 2, 3);
Mitos e verdades sobre performance
A única vez em que o objeto arguments não é criado é quando é declarado como um nome dentro de uma função
ou declarado como um de seus parâmetros formais. Não importa se ele é usado ou não.
Ambos getters e setters são sempre criados; desta maneira, usá-los não causa impacto
de performance, especialmente não em código do mundo real, onde existe mais de um simples
acesso às propriedades do objeto arguments.
Entretando, existe um caso em que a performance é drasticamente reduzida
em engines modernas de JavaScript. Este caso é o uso de arguments.callee
function foo() {
arguments.callee; // Faça alguma coisa com os objeto deta função
arguments.callee.caller; // e o calling do objeto da função
}
function bigLoop() {
for(var i = 0; i < 100000; i++) {
foo(); // Would normally be inlined...
}
}
Isto não somente acaba com possíveis ganhos de performance que resultariam de inlining,
mas também quebram o encapsulamento pois a função agora depende de uma chamada específica de contexto.
O uso de arguments.callee é fortemente desencorajado.
Construtores
Construtores em JavaScript ainda são diferentes de muitas outras linguagens.
Qualquer chamada a uma função que seja precedida pela palavra-chave new age como um cosntrutor.
Dentro do construtor - a função chamada - o valor de this se refere ao objeto recém criado.
O prototype deste novo objeto é definido como o prototype do objeto da função que foi
invocada como construtor.
Se a função chamada não possui um return statement explícito, então implicitamente
retornará o valor de this - o novo objeto.
function Foo() {
this.bla = 1;
}
Foo.prototype.test = function() {
console.log(this.bla);
};
var test = new Foo();
O código acima chama Foo como construtor e define o prototype do objeto recém criado
como Foo.prototype.
No caso de um return statement explícito, a função retorna o valor
especificado pelo statement, mas somente se o valor de retorno for um Object.
function Bar() {
return 2;
}
new Bar(); // um novo objeto
function Test() {
this.value = 2;
return {
foo: 1
};
}
new Test(); // o objeto retornado
Quando a palavra-chave new é omitida, a função não retornará o novo objeto.
function Foo() {
this.bla = 1; // esta definida no escopo global do objeto
}
Foo(); // undefined
Enquanto que o exemplo acima pareça funcionar em alguns casos, devido
a maneira como this funciona em JavaScript, o objeto global
será usado como valor do this.
Fábricas
A fim de omitir a palavra-chave new, o construtor da função deve retornar um valor explicitamente.
function Bar() {
var value = 1;
return {
method: function() {
return value;
}
}
}
Bar.prototype = {
foo: function() {}
};
new Bar();
Bar();
Ambas as chamadas a Bar retornam a mesma coisa, um objeto recém criado
que tem a propriedade chamada method, que é uma Closure.
Deve-se perceber que a chamada new Bar()não afeta o prototype do objeto retornado.
Enquanto o prototype é definido no objeto recém criado, Bar nunca retornará um novo objeto.
No exemplo acima, não existe diferença funcional entre o uso ou não de new.
Criando novos objetos por fábricas
É recomendado não usar new pois eventual o esquecimento de seu uso
pode levar à defeitos.
A fim de criar um novo objeto, deve-se utilizar uma fábrica e construir o novo objeto dentro desta fábrica.
Enquanto que o código acima previne defeitos decorrentes do esquecimnto da palavra-chave new
e certamente utiliza-se de private variables de forma mais fácil, este apresenta algumas desvantagens:
Utiliza mais memória desde que os objetos criados não compartilham métodos em um prototype.
A fim de implementar herença, a fábrica precisa copiar todos os métodos de um outro objeto ou colocar o outro objeto no prototype do novo objeto.
Quebrar a cadeia prototype somente por causa de esquecer eventualmente o new vai contra o que é proposto pela linguagem.
Conclusão
Enquanto que a omissão do new origine defeitos, não é certamente uma razão para
quebrar a estrura prototype como um todo. No final, a melhor solução é sempre a que se adequa às necessidades de cada projeto.
O importante é utilizar de forma consistente o modelo de criação de objetos escolhido.
Escopos e Namespaces
Embora o JavaScript lide bem com a sintaxe de duas chaves para definir blocos, ele não oferece suporte a escopos em blocos; por isso,
todo o restante da linguagem é definido em escopo de função.
function test() { // define escopo
for(var i = 0; i < 10; i++) { // não define escopo
// count
}
console.log(i); // 10
}
Também não existem namespaces distintos em JavaScript, o que significa que tudo é
automaticamente definido em um namespace globalmente compartilhado.
Cada vez que uma variável é referenciada, o JavaScript vai percorrer toda a hierarquia
de escopos até encontrá-la. Caso ele alcance o escopo global e ainda não tenha encontrado
a variável, ele lançará um ReferenceError.
A queda das variáveis globais
// script A
foo = '42';
// script B
var foo = '42'
O dois scripts acima não têm o mesmo efeito. O Script A define uma
variável chamada foo no escopo global, e o Script B define foo no
escopo atual.
Novamente, isto não é mesma coisa: emitir o uso de var tem várias implicações.
// escopo global
var foo = 42;
function test() {
// escopo local
foo = 21;
}
test();
foo; // 21
Deixando o statement var de fora da função test faz com que o valor de test seja sobrescrito.
Enquanto que à primeira vista isto não pareça um problema, um script com milhares de linhas de código
que não utiliza var apresenta erros horríveis e bugs difíceis de serem detectados.
// escopo global
var items = [/* uma lista qualquer */];
for(var i = 0; i < 10; i++) {
subLoop();
}
function subLoop() {
// escopo de subLoop
for(i = 0; i < 10; i++) { // esquecendo do var statement
// faça algo incrível!
}
}
O loop externo terminará depois da primeira chamada para subLoop, uma vez que subLoop
sobrescreve o valor global i. Utilizar var no segundo for loop evitaria facilmente este problema.
O var statement nunca pode ser esquecido a não ser que o efeito desejado seja afetar o escopo externo.
Variáveis locais
A única fonte de variáveis locais em JavaScript são parâmetros de função
e variáveis declaradas via var statement.
// escopo global
var foo = 1;
var bar = 2;
var i = 2;
function test(i) {
// escopo local da função test
i = 5;
var foo = 3;
bar = 4;
}
test(10);
Enquanto que foo e i são variáveis locais dentro do escopo da função test,
a atribuição de bar irá substituir a variável global com o mesmo nome.
Hoisting
Javascript eleva declarações. Isto quer dizer que ambas declarações var
e function serão movidas para o topo do escopo ao qual pertencem.
bar();
var bar = function() {};
var someValue = 42;
test();
function test(data) {
if (false) {
goo = 1;
} else {
var goo = 2;
}
for(var i = 0; i < 100; i++) {
var e = data[i];
}
}
O código acima é modificado antes mesmo que seja executado. O JavaScript move
todos as declarações var assim como as de function, para o topo
do escopo mais próximo.
// declarações var são movidas aqui
var bar, someValue; // default para 'undefined'
// as declarações de função também são movidas
function test(data) {
var goo, i, e; // escopo de bloco ausente move essas variáveis p/ cá
if (false) {
goo = 1;
} else {
goo = 2;
}
for(i = 0; i < 100; i++) {
e = data[i];
}
}
bar(); // falha com um TypeError uma vez que bar continua 'undefined'
someValue = 42; // atribuições não são afetadas pelo hoisting
bar = function() {};
test();
A falta de delimitação de um escopo não somente moverá var statements para fora de loops
e seus blocos, isto também fará com que os testes de determinados if se tornem não-intuitivos.
No código original, embora o if statement pareça modificar a variável globalgoo, ele modifica a variável local - depois que hoisting foi aplicado.
Por desconhecer o hoisting, pode-se suspeitar que o código abaixo lançaria um
ReferenceError.
// verifique se SomeImportantThing já foi inicializado
if (!SomeImportantThing) {
var SomeImportantThing = {};
}
Mas é claro, isto funciona devido ao fato de que var statement é movido para o topo
do escopo global.
var SomeImportantThing;
// outro código pode inicializar SomeImportantThing aqui, ou não
// tenha certeza de que isto foi inicializado
if (!SomeImportantThing) {
SomeImportantThing = {};
}
Ordem da Resolução de Nomes
Todos os escopos em JavaScript, incluindo o escopo global, possuem o this
o qual faz referência ao objeto atual.
Escopos de funções também possuem o arguments, o qual contêm
os argumentos que foram passados para a função.
Por exemplo, ao tentar acessar a variável denominada foo dentro do escopo de uma função, JavaScript irá
procurar pela variável na seguinte ordem:
No caso de haver var foo statement no escopo atual, use-a.
Se um dos parâmetros é denominado foo, use-o.
Se a própria função é denominada foo, use-a.
Vá para o escopo externo mais próximo e inicie do #1.
Namespaces
Um problema comum relacionado ao fato de dispor de apenas um namespace global é
a probabilidade de esbarrar com problemas onde nomes de variáveis coincidem. Em JavaScript,
este problema pode ser facilmente evitado com a ajuda de wrappers anônimos.
(function() {
// um "namespace" autocontido
window.foo = function() {
// closure exposta
};
})(); // execute a função imediatamente
Funções sem nome são consideradas expressões; a fim de ser referênciável,
elas devem ser avaliadas.
( // avalie a função dentro dos parênteses
function() {}
) // e retorne o objeto de função
() // chama o resultado da avaliação
Existem outras maneiras para avaliar e chamar diretamente a expressão da função a qual,
enquanto que diferentes em sintaxe, comportam-se da mesma maneira.
// Alguns outros estilos para invocar diretamente ...
!function(){}()
+function(){}()
(function(){}());
// e assim por diante...
Conclusão
É recomendado sempre utilizar um wrapper anônimo para encapsular código em seu
próprio namespace. Isto não é somente uma maneira de se proteger contra conflitos de nomes,
como também contribui para melhor modularização de programas.
Adicionalmente, o uso de variáveis globais é considerado uma prática ruim. Qualquer
uso delas indica código mal escrito que tende à apresentar erros e apresenta manutenção complexa.
Arrays
Iteração com Arrays e propriedades
Embora arrays em JavaScript sejam objetos, não existem boas razões para utilizar
o for in loop. De fato, existem muitas boas razões para
evitar o uso de for in com arrays.
Uma vez que o for in loop enumera todas as propriedades que estão na cadeia
prototype e visto que o único modo de excluir tais propriedades é por meio do uso
do hasOwnProperty, ele chega a ser vinte vezes
mais custoso que o uso normal do for loop.
Iteração
A fim de atingir a melhor performance ao interagir sobre arrays,
a melhor opção é utilizar o clássico for loop.
var list = [1, 2, 3, 4, 5, ...... 100000000];
for(var i = 0, l = list.length; i < l; i++) {
console.log(list[i]);
}
Existe um detalhe importante no exemplo acima , que é o caching
do comprimento do array via l = list.length.
Embora a propriedade length esteja definida no próprio array, ainda existe
um trabalho extra ao executar a busca em cada iteração do array.
Enquanto que recentes engines JavaScript talvez implementem um otimização
para este caso, não existe uma maneira de saber quando o código será executado em uma
dessas novas engines.
De fato, deixando de lado o armazenamento em caching pode resultar em um loop duas vezes mais rápido
do que com o armazenamento em caching.
A propriedade length
Enquanto que o getter da propriedade length retorna o total de elementos
que estão contidos no array, o setter pode ser usado para truncar o array.
Atribuir um valor de menor para length trunca o array. Por outro lado, incrementando
o valor de length cria um array esparso.
Conclusão
Para melhor performance, é recomendado o uso do for loop e o cache da propriedade
length. O uso do for in loop na iteração com array é um sinal de código mal escrito
e tendencioso a apresentar defeitos, além de ter performance ruim.
O construtor Array
Uma vez que o construtor Array é ambíguo na forma como ele lida com seus parâmetros,
o uso da notação [] é fortemente recomendado ao criar novo arrays.
Nos casos onde somente um argumento é passado para o construtor Array e quando o argumento é
um Number, o construtor retornará um novo array sem elementos com a propriedade length configurada de acordo com o valor do argumento.
É importante perceber que somente a propriedade length do novo array será configurada desta maneira; os índices do array não serão inicializados.
var arr = new Array(3);
arr[1]; // undefined
1 in arr; // false, o índice não foi definida
Ser capaz de definir o comprimento de um array antecipadamente é útil em poucos casos,
como ao replicar uma string, em que se evita o uso de um loop.
new Array(count + 1).join(stringToRepeat);
Conclusão
O uso de literais é preferencial na inicialição de Arrays. São curtos, possuem uma sintaxe limpa, e contribuem para a legibilidade do código.
Tipos
Igualdades e comparações
JavaScript tem duas maneiras diferentes de comparar a igualdades entre valores de objetos.
O operador de igualdade
O operador de igualdade consiste de dois sinais de igual : ==
JavaScript é fracamente tipado. Isto que dizer que o operador de igualdade
induz tipos ao invés de compará-los.
A tabela acima mostra o resultado da coerção de tipos, e isto é principal razão
para que o uso == seja amplamente considerado uma má prática. Seu uso introduz defeitos
difíceis de serem rastreados devido às suas complicadas regras de conversão.
Adicionalmente, também existe um impacto em performance quando a coerção acontece;
por exemplo, é necessário que uma string seja convertida em um número antes que seja comparada
com outro número.
O operador de igualdade estrito
O operador de igualdade estrito consiste de três sinais de igual : ===.
Ele funciona como o operador de igualdade normal, salvo que o operador de igualdade estrito
não realiza coerção de tipos entre seus operandos.
Os resultados acima são bastante claros e permitem uma análise objetiva do código. Pode parecer complicar o código até um certo ponto
mas também traz ganhos de performance em casos em que os operandos são de tipos diferentes.
Comparando Objetos
Enquanto que ambos == e === são denominados operadores de igualdade, eles se comportam de
formas diferentes quando pelo menos um de seus operandos é um Object.
{} === {}; // false
new String('foo') === 'foo'; // false
new Number(10) === 10; // false
var foo = {};
foo === foo; // true
Aqui, ambos os operadores comparam em função da identidade e não da igualdade; isto é,
eles vão comparar em função da mesma instância do objeto, muito parecido com o is do Python
e a comparação de ponteiros em C.
Conclusão
E fortemente recomendado que só se use o operador de igualdade estrito.
Em casos onde a coerção de tipos seja necessária, isto deve ser feito explicitamente
e não deve ser deixado para as complicadas regras de coerção da linguagem.
O operador typeof
O operador typeof(em conjunto com
instanceof é provavelmente a maior falha de design do JavaScript,
por estar complemente mal implementado.
Embora instanceof ainda tenha seu uso limitado, typeof realmente só possui uma utilidade,
a qual não acaba por ser a de verificar o tipo de um objeto.
A tabela de tipos em JavaScript
Value Class Type
-------------------------------------
"foo" String string
new String("foo") String object
1.2 Number number
new Number(1.2) Number object
true Boolean boolean
new Boolean(true) Boolean object
new Date() Date object
new Error() Error object
[1,2,3] Array object
new Array(1, 2, 3) Array object
new Function("") Function function
/abc/g RegExp object (function in Nitro/V8)
new RegExp("meow") RegExp object (function in Nitro/V8)
{} Object object
new Object() Object object
Na tabela acima, Type se refere ao valor de retorno do operador typeof.
Como pode ser facilmente observado, este valor não é nada consistente.
O Class se refere ao valor interno da propriedade [[Class]] de um objeto.
A fim de se obeter o valor de [[Class]], deve-se utilizar o método
toString de Object.prototype.
A classe de um objeto
A especificação fornece exatamente uma maneira de acessar o valor de [[Class]],
com o uso de Object.prototype.toString.
No exemplo acima, Object.prototype.toString é chamado enquanto que o valor de this
é definido como o objeto o qual o valor [[Class]] deva ser retornado.
Teste para variáveis não-definidas
typeof foo !== 'undefined'
O exemplo acima irá verificar se foo foi declarado ou não; apenas
o fato de referênciá-lo poderia resultar em ReferenceError. Esta é a única utilidade
real de typeof.
Conclusão
A fim de verificar o tipo de um objeto, é fortemente recomendade o uso de
Object.prototype.toString pelo motivo de que esta é a única maneira confiável de ser feita.
Como demonstrado na tabela anterior, alguns valores retornados de typeof não estão definidos na
especificação; assim, eles podem variar entre implementações.
O uso de typeof deve ser evitado, a menos que não se esteja testando se uma variável está ou não definida.
O operador instanceof
O operador instanceof compara os construtores de seus dois operandos.
Ele é útil somente quando estamos comparando objetos personalizados. Quando utilizado em tipos nativos,
ele é tão inútil quanto o operador typeof.
Comparando objetos personalizados
function Foo() {}
function Bar() {}
Bar.prototype = new Foo();
new Bar() instanceof Bar; // true
new Bar() instanceof Foo; // true
// Isto somente define Bar.prototype ao objeto de função Foo,
// mas não à instância atual de Foo
Bar.prototype = Foo;
new Bar() instanceof Foo; // false
Uma coisa importante para perceber aqui é que instanceof não funciona em objetos
originados de diferentes contextos de JavaScript (isto é, de diferentes documentos em um
navegador web), uma vez que seus construtores não irão representar exatamente o mesmo objeto.
Conclusão
O operador instanceof deve somente ser utilizado quando estive lidando
com objetos customizados originados de um mesmo contexto JavaScript. Bem como o operador
typeof, qualquer outro uso de instanceof deve ser evitado.
Conversão de tipos
JavaScript é fracamente tipado, logo ele aplica a coerção de tipossempre que possível.
// Estes retornam true
new Number(10) == 10; // Number.toString() é convertido
// de volta a um número
10 == '10'; // Strings são convertidas em Number
10 == '+10 '; // Mais loucuras com strings
10 == '010'; // E mais
isNaN(null) == false; // null é convertido em 0
// que claro não é NaN
// Estes retornam false
10 == 010;
10 == '-10';
A fim de evitar os problemas acima, o uso do operador de igualdade estrito
é fortemente recomendado. Embora ele evite uma série de problemas comuns,
existem ainda muitas outras questões que surgem do fraco sistema de tipagem do JavaScript.
Construtores de tipos nativos
Os construtores de tipos nativos como Number e String comportam-se
diferentemente quando utilizados com ou sem a palavra-chave new.
new Number(10) === 10; // False, Object e Number
Number(10) === 10; // True, Number e Number
new Number(10) + 0 === 10; // True, devido à conversão implícita
Utilizar um tipo nativo como Number como construtor iré criar um novo objeto Number,
porém omitir a palavra-chave new fará com que a função Number se comporte como
um conversor.
Além, passando valores literais ou não-objetos irá resultar em mais coerções
de tipos.
A melhor opção é converter para um dos três possíveis tipos de forma explícita.
Convertendo para String
'' + 10 === '10'; // true
Prefixando uma string vazia, qualquer valor pode ser facilmente convertido em uma string.
Convertendo para Number
+'10' === 10; // true
Ao utilizar o operador de soma unário, é possível converter um valor para Number.
Convertendo para Boolean
Ao utilizar duas vezes o operador not, é possível converter um valor para Boolean.
A função eval executará uma string de código JavaScript no escopo local.
var foo = 1;
function test() {
var foo = 2;
eval('foo = 3');
return foo;
}
test(); // 3
foo; // 1
Entretanto, eval somente é executado no escopo local quando é chamado diretamente
e quando o nome da função chamada é eval.
var foo = 1;
function test() {
var foo = 2;
var bar = eval;
bar('foo = 3');
return foo;
}
test(); // 2
foo; // 3
O uso de eval deve ser evitado. 99.9% de seu "uso" pode ser alcançado sem ele.
eval dissimulado
As funções timeoutsetTimeout e setInterval podem ambas receberem uma string
como primeiro argumento. Tais strings sempre serão executadas no escopo global uma vez que
eval não é chamado diretamente, naquele caso.
Problemas de segurança
eval também é considerado um problema de segurança, por que executa qualquer código dado.
Ele nunca deve ser utilizado com strings de origens duvidosas ou desconhecidas.
Conclusão
eval nunca deve ser utilizado. Qualquer código que faça uso de eval seve ser questionado
em sua utilidade, performance e segurança. Se algo necessita de eval para funcionar, então não deve ser utilizado.
Um design melhor deve ser utilizado, um que não faça uso de eval.
undefined e null
JavaScript tem duas formas distintas para representar o nada, null e undefined, com
o último sendo o mais útil.
O valor undefined
undefined é um tipo com um valor exato : undefined.
A linguagem também define uma variável global que tem como valor undefined;
esta variável também é chamada undefined. Entretanto tal variável não é nem uma constante
muito menos uma palavra-chave da linguagem. Isto significa que seu valor pode ser facilmente
sobrescrito.
Aqui estão alguns exemplos de quando o valor undefined é retornado:
Acessando uma variável global (não-modificada) undefined.
Acessando uma variável declarada mas não ainda inicializada.
Retornos implícitos de funções devido ao esquecimento do return statement.
return statements que explicimente retornam o nada.
Busca por propriedades não-existentes.
Parâmetros de funções que não têm um valor explícito definido.
Qualquer coisa que tenha sido definida como undefined.
Qualquer expressão no formato void(expressão)
Manipulando mudanças no valor de undefined
Uma vez que a variável global undefined apenas mantém uma cópia do valor atual de undefined, atribuir-lhe
um novo valor não muda o valor do tipo undefined.
Ainda, a fim de comparar alguma coisa com o valor de undefined, é necessário que
primeiro se retorne o undefined.
A fim de proteger o código contra uma possível sobrescrtia da variável undefined, uma
técnica comum utilizada é a de adicionar um parâmetro adicional em um wrapper anônimo
que não recebe argumentos.
var undefined = 123;
(function(something, foo, undefined) {
// undefined no escopo local agora
// refer-se ao valor `undefined`
})('Hello World', 42);
Outra maneira de atingir o mesmo efeito seria utilizar uma declaração dentro do wrapper.
var undefined = 123;
(function(something, foo) {
var undefined;
...
})('Hello World', 42);
A única diferença aqui é a que a última versão resulta na redução de 4 bytes, e não existe
outro var statement dentro do wrapper anônimo.
Usos do null
Enquanto que undefined no contexto da linguagem JavaScript é normalmente utilizado
como um null, o atual null (ambos o tipo e o literal) é mais ou menos um outro tipo de dado.
Ele é utilizado internamente pelo JavaScript (como na declaração no fim da cadeia prototype
ao definir Foo.prototype = null), porém na maioria dos casos, pode ser substituido por undefined.
Inserção automática do ponto e vírgula
Apesar do JavaScript possuir uma sintaxe no estilo C, o uso do ponto e vírgula não é obrigatório.
JavaScript não é uma linguagem 'semicolon-less'. De fato, o ponto e vírgula é necessário para o interpretação do código. Entretanto, o parser do JavaScript insere o ponto e vírgula automaticamente sempre que ocorrer um error de parser, decorrente da falta do ponto e vírgula.
A inserção acontece e o parser realiza uma nova tentativa.
var foo = function() {
}; // no error, parser continues
test()
A inseção automática de ponto e vírgula é considerada um dos maiores equívocos no design da linguagem pois pode influenciar no comportamento do código.
Como funciona
O código abaixo não possui ponto e vírgula, então fica à cargo do parser inserir o ponto e vírgula onde julgar necessário.
(function(window, undefined) {
function test(options) {
log('testing!')
(options.list || []).forEach(function(i) {
})
options.value.test(
'long string to pass here',
'and another long string to pass'
)
return
{
foo: function() {}
}
}
window.test = test
})(window)
(function(window) {
window.someLibrary = {}
})(window)
Abaixo está o resultado do processamento do parser.
(function(window, undefined) {
function test(options) {
// Not inserted, lines got merged
log('testing!')(options.list || []).forEach(function(i) {
}); // <- inserted
options.value.test(
'long string to pass here',
'and another long string to pass'
); // <- inserted
return; // <- inserted, breaks the return statement
{ // treated as a block
// a label and a single expression statement
foo: function() {}
}; // <- inserted
}
window.test = test; // <- inserted
// The lines got merged again
})(window)(function(window) {
window.someLibrary = {}; // <- inserted
})(window); //<- inserted
O parser mudou o comportamento do código acima drásticamente. Em determinados casos, o parser não procede como o esperado.
Parênteses
No caso de parênteses, o parser não insere o ponto e vírgula.
As chances de log não retornar uma função são muito altas; portanto, o código acima irá produzir um TypeError informando que undefined is not a function.
Conclusão
É fortemente recomendado que nunca se omita o ponto e vírgula. Também é recomendado que chaves sejam mantidas na mesma linha que seus statements e que nunca sejam omitadas em declações de uma só linha como if / else statements. Tais medidas não somente melhorarão a consistência do código, como também irão previnir alteração no comportamento do código por má interpretação do parser do JavaScript.
O operador delete
Em resumo, é impossível remover variáveis globais, funções e outras coisas em JavaScript
que tenham o atributo DontDelete definido.
Código global e código de função
Quando uma variável ou função é definida no escopo global ou em
um escopo de função ela passa a ser uma propriedade de ambos
objeto Activation e do objeto Global. Tais propriedades possuem um conjunto de atributos, um dos quais é o DontDelete.
Declarações de funções e variáveis em código global e em código de função
sempre criam propriedades com DontDelete, e portanto não podem ser removidas.
// variável global:
var a = 1; // DontDelete está definido
delete a; // false
a; // 1
// função comum:
function f() {} // DontDelete está definido
delete f; // false
typeof f; // "function"
// mudar o valor do atributo não ajuda:
f = 1;
delete f; // false
f; // 1
Propriedades explícitas
Propriedades definidas explicitamente podem ser apagadas normalmente.
No exemplo acima, obj.x e obj.y podem ser removidos por que eles não possuem o
atributo DontDelete. Este é o motivo pelo qual o exemplo abaixo também funciona.
// Desconsiderando o IE, isto funciona bem:
var GLOBAL_OBJECT = this;
GLOBAL_OBJECT.a = 1;
a === GLOBAL_OBJECT.a; // true - apenas uma variável global
delete GLOBAL_OBJECT.a; // true
GLOBAL_OBJECT.a; // undefined
Aqui nós utilizamos um truque para remover a. Aqui o this
faz referência ao objeto Global e declara explicitamente a variável a como
sua propriedade a qual nos permite removê-la.
O IE (pelo menos 6-8) possui defeitos, então o código acima não funciona.
Argumentos de função e propriedades nativas
Argumentos de função, objetos arguments e
propriedades nativas tambêm possuem o DontDelete definido.
O comportamento do operador delete pode ser imprevisível para objetos hosts.
Devido a especificação, objetos hosts têm permissão para implementar qualquer tipo de comportamento.
Conclusão
O operador delete freqüentemente apresenta um comportamento inesperado e só
pode ser usado com segurança para remover propriedades definidas explicitamente em objetos normais.
Outros assuntos
setTimeout e setInterval
Uma vez que JavaScript é assíncrono, é possível agendar a execução de uma função
usando as funções setTimeout e setInterval.
function foo() {}
var id = setTimeout(foo, 1000); // retorna um Number > 0
Quando setTimeout é chamado, ele retorna o ID do timeout e agenda a execução de foo
para aproximadamente mil milissegundos no futuro.
foo será executado uma única vez.
Dependendo de como a engine JavaScript que está rodando o código resolve o timer, bem como
o fato de que o JavaScript é single threaded e outro código que é executado pode bloquear a
thread, não há como garantir a precisão dos intervalos especificados nas chamadas setTimeout.
A função que foi passada como primeiro parâmetro será chamada pelo objeto global, o que
significa que o this dentro da função chamada se refere ao objeto global.
function Foo() {
this.value = 42;
this.method = function() {
// this faz referência ao objeto global
console.log(this.value); // log undefined
};
setTimeout(this.method, 500);
}
new Foo();
Acumulando chamadas com o setInterval
Enquanto que setTimeout somente executa a função uma vez, setInterval - como
o nome sugere - irá executar a função a cadaX milisegundos, porém seu uso é
desencorajado.
Quando um código em execução bloqueia a chamada do timeout, setInterval continuará
emitindo chamadas para a função em questão. Isto pode, especialmente com intervalos curtos,
resultar em uma pilha de chamadas de função.
function foo(){
// algo que bloqueie por 1 segundo
}
setInterval(foo, 1000);
No código acima, foo será chamada uma vez e irá então bloquear a execução por um segundo.
Enquanto foo bloqueia a execução, setInterval irá programar mais chamadas para ela.
Em seguida, quando foo completar sua execução, existirão dez chamadas programadas
para ela aguardando por execução.
Lidando com possíveis bloqueios de código
A solução mais fácil, bem como a mais controlável, é usar setTimeout dentro da
própria função.
function foo(){
// Algo que bloqueia por um segundo
setTimeout(foo, 1000);
}
foo();
Isto não somente encapsula a chamada para setTimeout, mas também previne
o acumulo de chamadas e dá controle adicional. foo por si só pode decidir
quando rodar novamente ou não.
Limpando Timeouts manualmente
A limpeza de intervalos e timeouts funciona passando o respectivo ID
para clearTimeout ou clearInterval, dependendo onde a função set foi usada primeiro.
var id = setTimeout(foo, 1000);
clearTimeout(id);
Limpando todos os Timeouts
Como não existe métodos próprios para limpar todos os timeouts e/ou intervalos,
é necessário usar a força bruta para chegar a esta funcionalidade.
// limpe "todos" os timeouts
for(var i = 1; i < 1000; i++) {
clearTimeout(i);
}
Mas ainda podem haver timeouts que não serão afetados por este número arbitrário.
Uma outra maneira de fazer isto é considerar que o ID dado a um timeout é
incrementado um a um cada vez que você chama setTimeout.
// limpe "todos" os timeouts
var biggestTimeoutId = window.setTimeout(function(){}, 1),
i;
for(i = 1; i <= biggestTimeoutId; i++) {
clearTimeout(i);
}
Apesar desta maneira funcionar nos principais navegadores hoje em dia, não está especificado
que os IDs respeitem uma ordem como este, logo esta ordem pode ser variada. Por este motivo, em vez disso
é recomendade manter o controle de todos os IDs de timeouts, de forma que possam ser apagados precisamente.
O uso oculto do eval
setTimeout e setInterval aceitam uma string como primeiro argumento.
Esta funcionalidade nunca deve ser utilizada pois internamente faz uso de eval.
function foo() {
// será chamada
}
function bar() {
function foo() {
// nunca será chamada
}
setTimeout('foo()', 1000);
}
bar();
Uma vez que eval não é chamado diretamente neste caso, a string
passada como argumento para setTimeout será executada no escopo global; assim, ela
não usará a variável local foo do escopo de bar.
Também é recomendado não usar uma string para passar argumentos
para a função que será chamada por qualquer uma das funções de timeout.
function foo(a, b, c) {}
// NUNCA use isto
setTimeout('foo(1, 2, 3)', 1000)
// Utilize uma função anônima do lugar
setTimeout(function() {
foo(a, b, c);
}, 1000)
Conclusão
Uma string nunca deve ser usada como parâmetro setTimeout ou
setInterval. Esta prática é um sinal claro de código ruim, quando argumentos precisam ser fornecido para a função que é chamada.
Uma função anônima é que deve ser passada para que, em seguida, cuide da chamada.
Além disso, o uso de setInterval deve ser evitado pois seu scheduler não é
bloqueado pela execução do JavaScript.