Avançando com JS: Escopo & Closures

Gustavo Valle
Mercafacil
Published in
8 min readApr 13, 2023

Neste artigo vamos discutir sobre esses dois conceitos de JavaScript e ver alguns exemplos de como podemos tirar proveito deles

Avançando com Javascript — Escopo e Closure
Avançando com JS

Começando pelo começo. Essa discussão está muito bem detalhada no livro “You Don’t Know JS: Scope & Closures” e eu realmente recomendo a leitura.

O objetivo desta série de livros é mostrar que você pode não conhecer JavaScript, mas apenas saber como fazer as coisas funcionarem e como utilizar a linguagem para isso. Se você deseja entender melhor a linguagem e utilizar todo o seu potencial, é necessário aprofundar-se em tópicos, como aqueles que serão abordados hoje, para explicar determinados conceitos e usar funcionalidades avançadas da linguagem.

Em JavaScript, o escopo refere-se ao contexto no qual as variáveis e funções são definidas e podem ser acessadas. O escopo pode ser global, local ou de bloco.

Escopo Global

As variáveis e funções definidas fora de qualquer função ou bloco de código são definidas no escopo global e podem ser acessadas em qualquer lugar do código. No entanto, as variáveis globais podem ser difíceis de gerenciar e podem levar a problemas de nomenclatura e colisões de nomes.

Escopo Local

As variáveis e funções definidas dentro de uma função são definidas no escopo local e só podem ser acessadas dentro da função ou em funções aninhadas. Variáveis definidas dentro de uma função são destruídas quando a função é concluída.

Escopo de Bloco

A partir do ES6, o JavaScript introduziu o escopo de bloco, o que significa que variáveis e funções definidas dentro de um bloco de código (por exemplo, um loop for ou um bloco if) só podem ser acessadas dentro desse bloco.

Closures

Uma closure é uma função que tem acesso a variáveis em seu escopo externo, mesmo após a execução da função externa ter sido concluída. Isso é possível porque a função interna (closure) mantém uma referência ao escopo em que foi criada, o que permite o acesso a variáveis desse escopo mesmo após a função externa ter retornado.

As closures são usadas ​​para criar funções que têm acesso a variáveis externas que não estão disponíveis em seu escopo local. Isso permite que as funções sejam criadas com um comportamento personalizado e flexível.

Por exemplo, imagine uma função que retorna outra função que soma um número ao número passado na primeira função:

function soma(x) {
return function(y) {
return x + y;
};
}

let soma5 = soma(5);
console.log(soma5(3)); // output: 8

Nesse exemplo, a função interna (closure) tem acesso à variável x definida no escopo da função externa soma(). Isso permite que a função interna some o valor de x ao valor passado como argumento. Ao chamar soma(5), uma nova função é retornada com x igual a 5. Em seguida, a nova função é atribuída a soma5 e chamada com o valor 3. O resultado é 8, que é a soma de 5 e 3.

As closures permitem que você crie funções com comportamentos personalizados e flexíveis, enquanto o entendimento do escopo ajuda a evitar problemas de nomenclatura e gerenciar variáveis em seu código.

Exemplo Prático

Vamos supor que temos um objeto Cart que contém informações sobre o carrinho de compras de um usuário, incluindo a lista de produtos, seus preços e quantidades. Precisamos de uma função para calcular o valor total do carrinho, levando em conta o preço de cada produto e a quantidade comprada.

Podemos definir uma função para isso, usando closures para acessar as informações do carrinho:

function createCalculateTotal(cart) {
return function () {
let total = 0;
cart.products.forEach((product) => {
total += product.price * product.quantity;
});
return total;
};
}

const cart = {
products: [
{ name: 'Product A', price: 10, quantity: 2 },
{ name: 'Product B', price: 15, quantity: 1 },
{ name: 'Product C', price: 5, quantity: 3 },
],
};

const calculateTotal = createCalculateTotal(cart);

console.log(calculateTotal()); // 50

Nesse exemplo, a função createCalculateTotal retorna uma função interna que calcula o valor total do carrinho, usando as informações contidas no objeto cart. A função interna tem acesso ao escopo externo, onde as informações do carrinho são armazenadas.

Podemos usar essa função para criar funções personalizadas para cada cliente, adicionando descontos específicos para cada um. Por exemplo:

function createCalculateTotal(cart, discount) {
return function () {
let total = 0;
cart.products.forEach((product) => {
total += product.price * product.quantity;
});
return total * (1 - discount);
};
}

const cart = {
products: [
{ name: 'Product A', price: 10, quantity: 2 },
{ name: 'Product B', price: 15, quantity: 1 },
{ name: 'Product C', price: 5, quantity: 3 },
],
};

const customer1Discount = 0.1; // 10% off
const customer2Discount = 0.2; // 20% off

const calculateTotalForCustomer1 = createCalculateTotal(cart, customer1Discount);
const calculateTotalForCustomer2 = createCalculateTotal(cart, customer2Discount);

console.log(calculateTotalForCustomer1()); // 45
console.log(calculateTotalForCustomer2()); // 40

Nesse exemplo, a função createCalculateTotal agora recebe um parâmetro adicional discount, que é usado para calcular o valor total com base no desconto aplicado para cada cliente. Podemos criar duas funções personalizadas para cada cliente, usando descontos diferentes, e calcular o valor total do carrinho para cada um.

Outra possibilidade muito interessante que se abre é a de adicionarmos um novo produto ao carrinho do usuário:

function createCalculateTotal(cart) {
return function () {
let total = 0;
cart.products.forEach((product) => {
total += product.price * product.quantity;
});
return total;
};
}

const cart = {
products: [
{ name: 'Product A', price: 10, quantity: 2 },
{ name: 'Product B', price: 15, quantity: 1 },
{ name: 'Product C', price: 5, quantity: 3 },
],
};

const calculateTotal = createCalculateTotal(cart);

console.log(calculateTotal()); // 50

cart.products.push({ name: 'Product D', price: 20, quantity: 2 }); // +1 produto

console.log(calculateTotal()); // 90

Veja que, dessa forma, quando adicionamos um produto ao carrinho, adicionando a instrução cart.products.push({ name: "Product D", price: 20, quantity: 2 }) ela é executada fora da função createCalculateTotal, adicionando um novo objeto à propriedade products do objeto original referenciado pela variável cart. Como a função createCalculateTotal utiliza a referência ao objeto original, e não uma cópia, a adição do novo objeto é refletida no cálculo do total quando a função é executada novamente.

Assim, ao chamar calculateTotal() depois de adicionar o novo produto ao carrinho, a função ainda está utilizando a mesma referência ao objeto original que foi passada na criação da função. Como resultado, a função calcula corretamente o novo valor total com base nos produtos atualizados no carrinho.

Referências left-hand-side e right-hand-side

Em JavaScript, uma expressão é avaliada em duas fases: a fase de compilação e a fase de execução. Durante a fase de compilação, o interpretador da linguagem realiza uma análise do código e detecta as variáveis que serão usadas na aplicação. Nesse processo, ele categoriza as referências a essas variáveis em dois tipos: referência LHS (Left Hand Side) e referência RHS (Right Hand Side).

  • Referência LHS: quando a variável é o alvo da atribuição. Por exemplo, em uma instrução como x = 10, a referência é LHS porque o valor é atribuído à variável x.
function foo(a) {
a = 10; // referência LHS
}

Outro exemplo seria em uma declaração de função: function bar(a) {}. Neste caso, a referência é LHS porque a variável a está sendo declarada naquele escopo.

  • Referência RHS: quando o valor da variável é buscado para ser usado em alguma operação. Por exemplo, em uma instrução como console.log(x), a referência é RHS porque o valor de x está sendo buscado para ser usado na função console.log.
function foo(a) {
console.log(a); // referência RHS
}

Entender as referências RHS e LHS é fundamental para entender os conceitos de escopo e closure em JavaScript. Isso porque o comportamento de referências LHS e RHS afeta diretamente como as variáveis são acessadas e modificadas dentro do escopo de uma função.

Por exemplo, considerando a seguinte função:

function outer() {
var x = 10;
function inner() {
var y = 20;
console.log(x + y);
}
return inner;
}

var fn = outer();
fn();

Neste exemplo, outer() retorna a função inner(), que é atribuída à variável fn. Quando fn() é chamada, ela executa o código de inner(), que acessa a variável x definida em outer() e a variável y definida em inner().

No entanto, se a variável x não tivesse sido declarada corretamente dentro de outer() (por exemplo, esquecendo de usar a palavra-chave var), isso resultaria em uma referência LHS implícita no escopo global, o que afetaria o comportamento de inner().

Uma closure é um recurso da linguagem que permite que uma função acesse variáveis de um escopo externo, mesmo após o término da execução desse escopo. Isso é possível graças à referência LHS implícita, que é criada quando uma variável é declarada dentro de uma função e que permite que a variável seja mantida na memória mesmo após o término da execução da função.

Por exemplo, considerando o seguinte código:

function outer() {
var x = 10;
return function inner() {
console.log(x);
}
}

var fn = outer();
fn();

Neste exemplo, a função inner() tem acesso à variável x definida em outer(), mesmo após o término da execução de outer(), graças à closure. O entendimento das referências RHS e LHS é crucial para entender como a closure é criada e como a variável x é mantida na memória para ser acessada posteriormente por inner().

Por último, mas não menos importante: Escopo Léxico

Escopo léxico é um conceito importante em JavaScript que se refere à maneira como as funções em JavaScript procuram e acessam variáveis e outros recursos em seu ambiente de origem. O termo “léxico” se refere à ordem física em que o código é escrito no arquivo e como as funções são aninhadas umas dentro das outras.

Em termos simples, um escopo léxico determina quais variáveis e recursos estão disponíveis para uma função em um determinado momento durante a execução do código. Cada vez que uma nova função é declarada em JavaScript, um novo escopo léxico é criado para ela. Esse escopo léxico contém as variáveis, funções e outros recursos que a função pode acessar.

O escopo léxico é baseado em onde uma função é definida dentro do código. Quando uma função é declarada dentro de outra função, ela tem acesso ao escopo léxico da função externa, incluindo todas as variáveis e recursos disponíveis naquele escopo. No entanto, o contrário não é verdadeiro: a função externa não tem acesso ao escopo léxico interno da função interna.

Vejamos um exemplo simples:

function externalFunction() {
var externalVar = 'I am outside!';
function internalFunction() {
var internalVar = 'I am inside!';
console.log(externalVar); // 'I am outside!'
console.log(internalVar); // 'I am inside!'
}
internalFunction();
}

externalFunction();

Neste exemplo, internalFunction() tem acesso ao escopo léxico da externalFunction(), que contém a variável externalVar. No entanto, externalFunction() não tem acesso ao escopo léxico da internalFunction(), que contém a variável internalVar.

O escopo léxico é uma das principais características que tornam JavaScript uma linguagem funcional poderosa. Ele permite que as funções acessem variáveis e recursos de seus respectivos ambientes, o que é fundamental para o funcionamento correto do código.

Concluindo

Em resumo, o entendimento dos conceitos de escopo, closure e escopo léxico em JavaScript é fundamental para o desenvolvimento e entendimento do real comportamento de aplicações robustas. Esses conceitos permitem que as variáveis e recursos sejam acessados apenas onde e quando forem necessários, evitando a exposição de informações desnecessárias e reduzindo a chance de erros no código.

O escopo refere-se ao conjunto de variáveis e recursos que estão disponíveis em uma determinada parte do código, enquanto o closure permite que as funções mantenham referência a essas variáveis, mesmo que sejam chamadas em um contexto diferente.

Por fim, o escopo léxico permite que as funções tenham acesso apenas às variáveis e recursos definidos dentro de seu próprio escopo e nos escopos superiores na cadeia de escopos, garantindo a privacidade e a segurança dos dados do usuário em aplicações complexas como um ecommerce.

Em conjunto, esses conceitos formam a base para uma programação mais segura, organizada e eficiente em JavaScript e outras linguagens de programação que utilizam escopo léxico e closure.

Dúvidas, sugestões, críticas, reclamações, anseios e aflições, sinta-se a vontade para entrar em contato comigo.

--

--