Jedi Tux

JavaScript, Laços, Closures e Escopo

Posted in JavaScript by Fernando Basso on 11 de fevereiro de 2014

A função abaixo itera sobre um laço for, mas o resultado não é o esperado.

function fn() {
    var arr = [],
        i   = 0;

    for ( ; i < 3; ++i ) {
        arr[ i ] = function() {
            return i + 10;
        }
    }
    return arr;
}
var res = fn();

O objetivo do laço é popular o array, fazendo com que cada posição contenha uma função, de forma que seja possível chamar as funções dessa forma:

res[ 0 ](); // 10.
res[ 1 ](); // 11.
res[ 2 ](); // 12.

O problema é que devido à maneira que o escopo é tratado em Javascript, os resultados são 13, 13 e 13 em vez dos esperados 10, 11, 12.

O que aconteee é que em JavaScript as variáveis tem seu valor estabelecido no momento em que são usadas, e não no momento em que são declaradas. No caso do return i + 10, o i só é interpretado quando executamos res[ 0 ](), res[ 1 ](), etc. A função assinada a cada índice do array guarda apenas uma referência ao escopo onde foram definidas. Mais tarde, quando essas funções são realmente chamadas, elas vão encontrar o valor do i, que é 3 (último incremento antes da condição do laço retornar falso) e usar aquele valor.

Uma maneira de resolver o problema é usando uma closure para “forçar” o escopo e retornar uma função que guarda os valores desejados do i. Para isso vamos utilizar uma IIFE (immediately invoked function expression) que aceita o i como parâmetro e assim cria um novo escopo onde o valor correto do i é lembrado quando cada função retornada for finalmente chamada.

Essa é a versão “semi-correta”.

function fn() {
    var arr = [],
        i   = 0;
    for ( ; i < 3; ++i ) {
        arr[ i ] = function( n ) {
            return function() {
                return n + 10;
            }
        }( i ));
    }
    return arr;
}

var res = fn();
console.log( res[ 0 ]() ); // 10
console.log( res[ 1 ]() ); // 11
console.log( res[ 2 ]() ); // 12

Por que “semi-correta”? Porque estamos criando uma nova função a cada volta do laço. Isso é desnecessário e custa mais ciclos do processador. Vamos criar uma versão onde a função já fica pronta, apenas esperando para ser usada.

function fn() {

    // Note que addTen retorna uma função que por sua vez retorna o arg + 10.
    function addTen( arg ) {
        return function() {
            return arg + 10;
        };
    }

    var arr = [],
        i   = 0;
    for ( ; i < 3; ++i ) {
        arr[ i ] = addTen( i );
    }
    return arr;
}

var res = fn();
console.log( res[ 0 ]() ); // 10
console.log( res[ 1 ]() ); // 11
console.log( res[ 2 ]() ); // 12

Veja que na função addTen, seria incorreto especificar argumento para a função interna – aquela que é retornada, pois não teríamos como passar o argumento sem chamar a função. Nesse caso, estaríamos retornando o valor da função, e não a função em si.

    function addTen( arg ) {
        // ERRADO!
        return function( n ) { // Não especifique argumento na assinatura da função.
            return n + 10;
        }; // Teriamos que usar ( arg ) nessa linha.
    }

O melhor é deixar assim (como no exemplo correto):

    function addTen( arg ) {
        return function() {
            return arg + 10;
        };
    }

Graças ao conceito de “closures”, mesmo arg não sendo passada como parâmetro na função interna (aquela que é retornada), ela ainda assim é acessível.

May the force be with you.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: