O escopo e o contexto em JavaScript são exclusivos da linguagem, em parte graças à flexibilidade que trazem. Cada função tem contexto e escopo de variáveis diferentes. Esses conceitos fundamentam alguns padrões de design poderosos em JavaScript. No entanto, isso também traz grande confusão aos desenvolvedores. A seguir, revelamos de forma abrangente as diferenças entre contexto e escopo em JavaScript e como vários padrões de design os utilizam.
contexto versus escopo
A primeira coisa que precisa ser esclarecida é que contexto e escopo são conceitos diferentes. Com o passar dos anos, percebi que muitos desenvolvedores frequentemente confundem esses dois termos, descrevendo incorretamente um como o outro. Para ser justo, esses termos tornaram-se muito confusos.
Cada chamada de função possui um escopo e um contexto associados a ela. Basicamente, o escopo é baseado em função e o contexto é baseado em objeto. Em outras palavras, o escopo está relacionado ao acesso às variáveis em cada chamada de função, e cada chamada é independente. O contexto é sempre o valor da palavra-chave this, que é uma referência ao objeto que chama o código executável atual.
escopo variável
As variáveis podem ser definidas em escopos locais ou globais, o que resulta no acesso a variáveis de tempo de execução de diferentes escopos. Variáveis globais precisam ser declaradas fora do corpo da função, existir durante todo o processo em execução e podem ser acessadas e modificadas em qualquer escopo. Variáveis locais são definidas apenas dentro do corpo da função e possuem um escopo diferente para cada chamada de função. Este tópico é atribuição, avaliação e operação de valores apenas dentro da chamada, não podendo ser acessados valores fora do escopo.
Atualmente, JavaScript não oferece suporte ao escopo em nível de bloco. O escopo em nível de bloco refere-se à definição de variáveis em blocos de instruções, como instruções if, instruções switch, instruções de loop, etc. Atualmente, quaisquer variáveis definidas em um bloco de instruções podem ser acessadas fora do bloco de instruções. No entanto, isso mudará em breve, já que a palavra-chave let foi oficialmente adicionada à especificação ES6. Use-o em vez da palavra-chave var para declarar variáveis locais como escopo em nível de bloco.
"este" contexto
O contexto geralmente depende de como uma função é chamada. Quando uma função é chamada como um método em um objeto, this é definido como o objeto no qual o método é chamado:
Copie o código do código da seguinte forma:
objeto var = {
foo: função(){
alerta(este === objeto);
}
};
object.foo();
O mesmo princípio se aplica ao chamar uma função para criar uma instância de um objeto usando o operador new. Quando chamado desta forma, o valor this será definido para a instância recém-criada:
Copie o código do código da seguinte forma:
função foo(){
alerta(este);
}
foo() // janela
novo foo() // foo
Ao chamar uma função não vinculada, isso será definido como o contexto global ou objeto de janela (se estiver em um navegador) por padrão. Porém, se a função for executada em modo estrito ("use strict"), o valor disto será definido como indefinido por padrão.
Contexto de execução e cadeia de escopo
JavaScript é uma linguagem de thread único, o que significa que só pode fazer uma coisa por vez no navegador. Quando o interpretador JavaScript executa inicialmente o código, primeiro ele assume como padrão o contexto global. Cada chamada a uma função cria um novo contexto de execução.
Muitas vezes ocorre confusão aqui. O termo "contexto de execução" aqui significa escopo, não contexto como discutido acima. Esta é uma nomenclatura inadequada, mas o termo é definido pela especificação ECMAScript e não há escolha a não ser obedecê-la.
Cada vez que um novo contexto de execução é criado, ele é adicionado ao topo da cadeia de escopo e se torna a execução ou pilha de chamadas. O navegador sempre é executado no contexto de execução atual, no topo da cadeia de escopo. Depois de concluído, ele (o contexto de execução atual) é removido do topo da pilha e o controle é retornado ao contexto de execução anterior. Por exemplo:
Copie o código do código da seguinte forma:
função primeiro(){
segundo();
função segundo(){
terceiro();
função terceiro(){
quarto();
função quarta(){
//faça alguma coisa
}
}
}
}
primeiro();
A execução do código anterior fará com que as funções aninhadas sejam executadas de cima para baixo até a quarta função. Neste momento, a cadeia de escopo de cima para baixo é: quarta, terceira, segunda, primeira, global. A quarta função pode acessar variáveis globais e quaisquer variáveis definidas na primeira, segunda e terceira funções, assim como suas próprias variáveis. Assim que a quarta função concluir a execução, o quarto contexto será removido do topo da cadeia de escopo e a execução retornará para a terceira função. Este processo continua até que todo o código tenha concluído a execução.
Os conflitos de nomenclatura de variáveis entre diferentes contextos de execução são resolvidos subindo a cadeia de escopo, de local para global. Isso significa que variáveis locais com o mesmo nome têm maior prioridade na cadeia de escopo.
Simplificando, toda vez que você tenta acessar uma variável no contexto de execução da função, o processo de busca sempre inicia a partir do próprio objeto variável. Se a variável que você está procurando não for encontrada em seu próprio objeto de variável, continue pesquisando na cadeia de escopo. Ele subirá na cadeia de escopo e examinará cada objeto variável de contexto de execução para encontrar um valor que corresponda ao nome da variável.
encerramento
Um encerramento é formado quando uma função aninhada é acessada fora de sua definição (escopo) para que possa ser executada após o retorno da função externa. Ele (o encerramento) mantém (na função interna) o acesso a variáveis locais, argumentos e declarações de função na função externa. O encapsulamento nos permite ocultar e proteger o contexto de execução do escopo externo, ao mesmo tempo que expõe a interface pública através da qual outras operações podem ser executadas. Um exemplo simples é assim:
Copie o código do código da seguinte forma:
função foo(){
var local = 'variável privada';
retornar barra de função(){
retornar localmente;
}
}
var getLocalVariable = foo();
getLocalVariable() // variável privada
Um dos tipos mais populares de fechamento é o conhecido padrão de módulo. Ele permite que você zombe de membros públicos, privados e privilegiados:
Copie o código do código da seguinte forma:
var Módulo = (função(){
var privateProperty = 'foo';
function métodoprivado(args){
//fazer algo
}
retornar {
propriedade pública: "",
método público: function(args){
//fazer algo
},
Método privilegiado: function(args){
métodoprivado(args);
}
}
})();
Os módulos são, na verdade, um tanto semelhantes aos singletons, adicionando um par de parênteses no final e executando-os imediatamente após o intérprete terminar de interpretá-los (executar a função imediatamente). Os únicos membros externos disponíveis do contexto de execução de fechamento são os métodos públicos e as propriedades no objeto retornado (como Module.publicMethod). Porém, todas as propriedades e métodos privados existirão durante todo o ciclo de vida do programa, pois o contexto de execução é protegido (fechamentos) e a interação com variáveis se dá através de métodos públicos.
Outro tipo de fechamento é chamado de expressão de função IIFE invocada imediatamente, que nada mais é do que uma função anônima auto-invocada no contexto da janela.
Copie o código do código da seguinte forma:
função(janela){
var a = 'foo', b = 'barra';
função privada(){
//faça alguma coisa
}
janela.Module = {
público: função(){
//faça alguma coisa
}
};
})(esse);
Esta expressão é muito útil para proteger o namespace global. Todas as variáveis declaradas no corpo da função são variáveis locais e persistem durante todo o ambiente de execução por meio de encerramentos. Essa forma de encapsular o código-fonte é muito popular tanto para programas quanto para frameworks, geralmente expondo uma única interface global para interagir com o mundo externo.
Ligue e inscreva-se
Esses dois métodos simples, integrados a todas as funções, permitem que funções sejam executadas em um contexto personalizado. A função call requer uma lista de parâmetros enquanto a função apply permite passar os parâmetros como um array:
Copie o código do código da seguinte forma:
function usuário(primeiro, último, idade){
//faça alguma coisa
}
user.call(janela, 'John', 'Doe', 30);
user.apply(janela, ['John', 'Doe', 30]);
O resultado da execução é o mesmo, a função do usuário é chamada no contexto da janela e os mesmos três parâmetros são fornecidos.
ECMAScript 5 (ES5) introduziu o método Function.prototype.bind para controlar o contexto, que retorna uma nova função que está permanentemente vinculada ao primeiro parâmetro do método bind, independentemente de como a função é chamada. Corrige o contexto da função através de encerramentos. Aqui está uma solução para navegadores que não a suportam:
Copie o código do código da seguinte forma:
if(!('bind' em Function.prototype)){
Função.prototype.bind=função(){
var fn = isto, contexto = argumentos[0], args = Array.prototype.slice.call(argumentos, 1);
função de retorno(){
return fn.apply(contexto, args);
}
}
}
É comumente usado em perda de contexto: orientação a objetos e processamento de eventos. Isso é necessário porque o método addEventListener do nó sempre mantém o contexto de execução da função como o nó ao qual o manipulador de eventos está vinculado, o que é importante. No entanto, se você usar técnicas avançadas de orientação a objetos e precisar manter o contexto da função de retorno de chamada como uma instância do método, deverá ajustar manualmente o contexto. Esta é a conveniência trazida pelo bind:
Copie o código do código da seguinte forma:
function MinhaClasse(){
this.element = document.createElement('div');
this.element.addEventListener('clique', this.onClick.bind(this), falso);
}
MinhaClasse.prototype.onClick = função(e){
//faça alguma coisa
};
Ao analisar o código-fonte da função bind, você pode notar a seguinte linha de código relativamente simples, chamando um método no Array:
Copie o código do código da seguinte forma:
Array.prototype.slice.call(argumentos, 1);
Curiosamente, é importante notar aqui que o objeto de argumentos não é na verdade um array, no entanto, é frequentemente descrito como um objeto semelhante a um array, muito parecido com o nodelist (o resultado retornado pelo método document.getElementsByTagName()). Eles contêm propriedades de comprimento e os valores podem ser indexados, mas ainda não são arrays porque não suportam métodos de array nativos, como slice e push. No entanto, como se comportam de maneira semelhante aos arrays, os métodos de array podem ser chamados e sequestrados. Se você deseja executar métodos de array em um contexto semelhante a um array, siga o exemplo acima.
Esta técnica de chamar métodos de outros objetos também é aplicada na orientação a objetos, ao emular herança clássica (herança de classe) em JavaScript:
Copie o código do código da seguinte forma:
MinhaClasse.prototype.init = function(){
//chama o método init da superclasse no contexto da instância "MyClass"
MySuperClass.prototype.init.apply(este, argumentos);
}
Podemos reproduzir esse poderoso padrão de projeto chamando métodos da superclasse (MySuperClass) em instâncias da subclasse (MyClass).
para concluir
É muito importante entender esses conceitos antes de começar a aprender padrões de design avançados, pois o escopo e o contexto desempenham um papel importante e fundamental no JavaScript moderno. Quer falemos sobre encerramentos, orientação a objetos e herança ou várias implementações nativas, o contexto e o escopo desempenham um papel importante. Se o seu objetivo é dominar a linguagem JavaScript e obter uma compreensão profunda de seus componentes, o escopo e o contexto devem ser o seu ponto de partida.
Suplemento do tradutor
A função de ligação implementada pelo autor está incompleta. Os parâmetros não podem ser passados ao chamar a função retornada por ligação.
Copie o código do código da seguinte forma:
if(!('bind' em Function.prototype)){
Função.prototype.bind=função(){
var fn = isto, contexto = argumentos[0], args = Array.prototype.slice.call(argumentos, 1);
função de retorno(){
return fn.apply(context, args.concat(argumentos));//fixo
}
}
}