profile picture

JavaScript: Qual a diferença entre Escopo e Contexto?

05 fevereiro de 2020

javascript programação conceitos

Alguns conceitos do JS (Javascript) são um pouco confusos para iniciantes. Um muito recorrente saber a diferença entre Escopo e Contexto. Este artigo é baseado nos vídeos da série What Makes Javascript Weird...and Awesome do canal do YouTube LearnCode.academy.

Escopo

Escopo tem a ver com acessibilidade de variáveis, seja dentro de if's, for's, funções e etc. Por exemplo, veja o trecho de código abaixo:

var nome = 'Lucas';

É uma simples declaração de variável. Digamos que o trecho acima esteja sendo executado dentro do browser. Variáveis declaradas no root scope (escopo global) são atribuídas ao objeto global window. Se no browser eu executar console.log(nome) será exibido no console: "Lucas".

As variáveis com um escopo maior, podem ser acessadas por escopos menores. O termo correto é Parent scope (Escopo pai) para o escopo mais alto e para o escopo menor é Child scope(Escopo filho). Porém isto não acontece inversamente, ou seja, váriaveis em escopos filhos não podem ser acessadas por escopos pai, veja o trecho a seguir:

var nome = 'Lucas';

function declaraSobrenome() {
  console.log(nome); // Lucas
  var sobrenome = 'Viana';
}

declaraSobrenome();

console.log(`${nome} ${sobrenome}`); // Uncaught ReferenceError: sobrenome is not defined

Há uma coisa interessante com os escopos. Já que os escopos filho não podem ser acessados por escopos pai, eles podem sobrescrever os escopos-pai (O termo para isto é name conflict). Ou seja, o código abaixo é válido:

var nome = 'Lucas';

function sobrescreveNome() {
  var nome = 'Jonas';
  console.log(window.nome); // Lucas
  console.log(nome); // Jonas
}

sobrescreveNome();

console.log(nome); // Lucas

E para finalizar, analise o código abaixo e responda: o que será exibido como nome no console?

var nome = 'Lucas';

function sobrescreveNome() {
  var nome = 'Jonas';
}

function sobrescreveNomeDenovo() {
  nome = 'Maria';
}

sobrescreveNome();
sobrescreveNomeDenovo();

console.log(nome); // O que será impresso no console?

Sim, você acertou. Será impresso "Maria". Isto acontece, pois antes do ES6 (onde temos diferentes formas de declarar variáveis com let e const) haviam muitos problemas com poluição do Root scope, já que uma declaração sem o var pode resultar em uma declaração global. Porém, faça o seguinte teste: rode o mesmo código do trecho anterior, porém com uma declaração "use strict" no topo do arquivo, você verá que não será possível sobrescrever o Root scope.

Contexto

É ligado à palavra especial this. Em uma analogia, pensando em nosso mundo, o contexto nada mais é do que o "espaço" que determinada variável ocupa (variável no sentido mais amplo, podendo ser uma função também) no ambiente.

Agora sendo um pouco mais o técnico, o this possui o mesmo comportamento do que qualquer outra variável, com a condição de que ele não pode ser mudado. Isto porque o this é alterado de outras formas que veremos a seguir.

O this referencia a um objeto de contexto, sendo um objeto podemos ter acesso a suas propriedades. Por exemplo, utilizando-se do exemplo anterior digamos que temos função que exibe um nome e sobrenome como o código a seguir:

var nome = 'Lucas';
var sobrenome = 'Viana';

function exibeNomeCompleto() {
  return `${this.nome} ${this.sobrenome}`;
}

console.log(exibeNomeCompleto()); // Lucas Viana

No ambiente de browsers, onde o JS pode ser executado, o contexto global para o this é a window. Como aprendemos na seção de Escopo, as variáveis globais são declaradas na window , portanto o this tem acesso a estas variáveis globais no exemplo anterior.

Agora por exemplo, e se eu tiver um novo objeto que referencia o this? No código abaixo é possível ver isto acontecer:

var nome = 'Lucas';
var sobrenome = 'Viana';

var Pessoa = {
  nome: 'José',
  sobrenome: 'Augusto',
  exibeNomeCompleto: function () {
    return `${this.nome} ${this.sobrenome}`;
  },
};

var exibeNomeCompleto = Pessoa.exibeNomeCompleto;

console.log(Pessoa.exibeNomeCompleto()); // José Augusto
console.log(exibeNomeCompleto()); // Lucas Viana

E porque no exemplo anterior os nomes foram exibidos com valores diferentes? Isto acontece pois em Pessoa o contexto estava contido em Pessoa , porém quando foi atribuído a uma variável no Root Scope o contexto mudou para Window .

Como dito anteriormente, não é possível mudar o valor de this, porém é possível alterar o contexto de alguma variável, existem 3 métodos para alterar o contexto de algo, eles são: call, apply e bind.

Call

O método call é responsável por executar uma função e em seu primeiro parâmetro é passado o contexto que será aplicado nesta execução, por exemplo:

var nome = 'Lucas';
var sobrenome = 'Viana';

var Pessoa = {
  nome: 'José',
  sobrenome: 'Augusto',
  exibeNomeCompleto: function () {
    return `${this.nome} ${this.sobrenome}`;
  },
};

console.log(Pessoa.exibeNomeCompleto.call(window)); // Lucas Viana

O método call aceita outros parâmetros que serão utilizados pela função executada, por exemplo:

var nome = 'Lucas';
var sobrenome = 'Viana';

var Pessoa = {
  nome: 'José',
  sobrenome: 'Augusto',
  saudacoes: function (cidade, sigla) {
    return `Olá ${this.nome} ${this.sobrenome}, bem-vindo a ${cidade}/${sigla}`;
  },
};

console.log(Pessoa.saudacoes.call(window, 'São Vicente', 'SP')); // Olá Lucas Viana, bem-vindo a São Vicente/SP

Apply

O método apply é basicamente a mesma coisa que o método call, com a única diferença que ele aceita apenas dois parâmetros (no método call você pode passar qualquer número de parâmetros) sendo o primeiro o contexto em que será executada a função e o segundo um array com os parâmetros.

A maioria das pessoas não entende a diferença entre os dois métodos (achando que servem para a mesma coisa, mas não). O método apply é útil no caso de você manipular dados previamente e querer os repassar para alguma função, no call você ficaria limitado em passar parâmetro a parâmetro os dados, já no apply você consegue manipular e passar diretamente para o método.

var Calculadora = {
  soma: function (...numeros) {
    return numeros.reduce((acc, cur) => acc + cur, 0);
  },
};

var numeros = Array(8)
  .fill(1)
  .map((_, index) => index + 1);

Calculadora.soma(1, 1, 2); // 4
Calculadora.soma.apply(window, numeros); // 36

Bind

O método bind é um pouco diferente, mas nem tanto. A diferença é que ele não executa a função, porém retorna sua declaração contendo o contexto passado a ele como parâmetro. Usando um dos exemplos anteriores, temos:

var nome = 'Jorge';
var sobrenome = 'Luiz';

var Pessoa = {
  saudacoes: function (cidade, sigla) {
    return `Olá ${this.nome} ${this.sobrenome}, bem-vindo a ${cidade}/${sigla}`;
  },
};

var saudarAlguem = Pessoa.saudacoes.bind(window);
saudarAlguem('Salvador', 'BA'); // // Olá Jorge Luiz, bem-vindo a Salvador/BA

Espero que tenham gostado da explicação sobre a diferença entre Escopo e Contexto no JavaScript, se tiver alguma dúvida, comentário ou sugestão procure por @mechamobau no Twitter e no GitHub.

Lembrando que este maravilhoso blog encontra-se em código aberto no meu GitHub. Caso queira ver o código ou realizar alguma melhoria visite https://github.com/mechamobau/blog. Eu agradeço 😄

Valeu meus queridos! ⚛️