Olá Habr!
Em um
artigo anterior, examinamos a teoria geral do OOP aplicada ao EcmaScript e a falácia popular de desenvolvedores iniciantes em relação às diferenças entre o OOP no JS e nas linguagens clássicas.
Hoje, falaremos sobre outros dois conceitos EcmaScript igualmente importantes, a saber, o relacionamento da entidade com o contexto de execução (
esta é essa conexão) e o relacionamento da entidade com o contexto de geração (
ScopeChain ).
Então, vamos começar!
isto
Nas entrevistas em resposta à pergunta: "Conte-nos mais sobre
isso ". Os desenvolvedores iniciantes, em regra, dão respostas muito vagas: "
este é o objeto" antes do ponto "usado para chamar o método", "
este é o contexto em que a função foi chamada" etc.
De fato, a situação com esse conceito, que é central para o EcmaScript, é um pouco mais complicada. Vamos descobrir em ordem.
Digamos que temos um programa JavaScript que possui variáveis declaradas globalmente; funções globais; funções locais (declaradas dentro de outras funções), funções retornadas de funções.
const a = 10; const b = 20; const x = { a: 15, b: 25, } function foo(){ return this.a + this.b; } function bar () { const a = 30; return a + b; } function fooBaz(){ function test () { return this.a + this.b; } return test(); } function fooBar() { const a = 40; const b = 50; return function () { return a + b; } } fooBar()();
Ao transferir o controle para o código executável, uma entrada é feita no contexto de execução. Código executável - este é qualquer código que executamos em um determinado momento, pode ser um código global ou um código de qualquer função.
O contexto de execução é uma abstração que tipifica e delimita o código. Do ponto de vista dessa abstração, o código é dividido em global (qualquer script conectado, script embutido) e código de função (o código das funções aninhadas não pertence ao contexto das funções pai).
Há um terceiro tipo - EvalCode. Neste artigo, nós a negligenciamos.
Logicamente, o conjunto de contextos de execução é uma
pilha que funciona de acordo com o princípio Last-in-First-out (lifo). A parte inferior da pilha é sempre o contexto global e a parte superior é o executável atual. Cada vez que uma função é chamada, uma entrada é feita em seu contexto. Quando uma função é concluída, seu contexto termina. Os contextos gastos são removidos da pilha sequencialmente e na ordem inversa.
Dê uma olhada no código acima. Temos uma chamada para a função
fooBar no código global. Na função
fooBar, retornamos
uma função anônima que chamamos imediatamente. As seguintes alterações ocorrem com a pilha: o
contexto global entra nele - quando
fooBar é chamado
, seu contexto entra na pilha - o contexto
fooBar termina, retorna
uma função anônima e é removido da pilha - uma
função anônima é chamada, seu contexto entra na pilha - uma
função anônima cumpre, retorna um valor e seu contexto é excluído da pilha - no final do script, o
contexto global é excluído da pilha.
O contexto de execução pode ser representado condicionalmente como um objeto. Uma das propriedades desse objeto será o Lexical Environment (LO).
O ambiente lexical contém:
- todas as declarações de variáveis de contexto
- todas as declarações de função
- todos os parâmetros formais da função (se estamos falando sobre o contexto das funções)
Ao entrar no contexto de execução, o intérprete verifica o contexto. Todas as declarações de variáveis e de funções chegam ao início do contexto. As variáveis são criadas iguais a indefinidas e as funções estão completamente prontas para uso.
isso também
é uma propriedade do contexto de execução, mas não do contexto em si, como alguns entrevistadores novatos respondem!
isso é definido ao entrar no contexto e permanece inalterado até o final da vida útil do contexto (até que o contexto seja removido da pilha).
No contexto de execução global,
isso é determinado dependendo do
modo estrito : quando o modo estrito está desativado, ele contém um objeto global (no navegador, ele é proxy para o nível superior no objeto da janela), com 'use strict' é indefinido.
isso no contexto de funções - a questão é muito mais interessante!
isso nas funções é determinado pelo chamador e depende da sintaxe da chamada. Por exemplo, como sabemos, existem métodos que permitem que isso seja corrigido rigidamente quando chamado (
chamar ,
aplicar ) e um método que permite criar um wrapper com "corrigido isso" (
ligação ). Nessas situações, declaramos isso explicitamente e não há dúvida sobre sua definição.
Com uma chamada de função normal, a situação é muito mais complicada!
Um dos tipos internos do EcmaScript, o
ReferenceType , nos ajudará a entender como isso é afixado nas funções. Este é um dos tipos internos disponíveis no nível de implementação. Logicamente, é um objeto com duas propriedades
base (uma referência a um determinado objeto base para o qual um ReferenceType é retornado),
propertyName (uma representação em cadeia do identificador de objeto para o qual um ReferenceType é retornado).
ReferenceType é retornado para todas as declarações de variáveis, declarações de funções e referências de propriedades (este é o caso que nos interessa do ponto de vista de entender isso).
A regra para definir
isso para funções chamadas da maneira usual:
Se o ReferenceType estiver à esquerda dos colchetes de ativação da função, a base desse ReferenceType será colocada this
função. Se qualquer outro tipo estiver à esquerda dos colchetes, this
é um objeto global ou undefined
(na verdade null
, mas como nulo não possui um valor específico do ponto de vista do ecmascript, ele é convertido em um objeto global, cuja referência pode ser é igual a undefined
dependendo do modo estrito).Vejamos um exemplo:
const x = 0; const obj = { x: 10, foo: function() { return this.x; } } obj.foo();
Eu acho que o método de definição é ilustrado claramente. Agora considere alguns casos menos óbvios.
Expressões funcionais
Vamos voltar ao nosso ReferenceType por um segundo. Esse tipo possui um método
GetValue interno que retorna o tipo verdadeiro do objeto recebido por meio do ReferenceType. Na zona de expressão, GetValue sempre é acionado.
Um exemplo:
(function (){ return this;
Isso ocorre porque o GetValue sempre é acionado na zona de expressão. GetValue retorna um tipo de função e, à esquerda dos colchetes de ativação, não é um ReferenceType. Lembre-se de nossa regra para determinar
isso :
se qualquer outro tipo estiver à esquerda dos colchetes, um objeto global será colocado this
ou undefined
(na verdade null
, mas como nulo não possui um determinado valor do ponto de vista do ecmascript, então ele é convertido em um objeto global , o link ao qual pode ser igual a indefinido, dependendo do modo estrito) .
As zonas de expressão são: atribuição (=), operadores || ou outros operadores lógicos, operador ternário, inicializador de matriz, lista separada por vírgula.
const x = 0; const obj = { x: 10, foo: function() { return this.x; } } obj.foo();
Situação idêntica nas expressões funcionais nomeadas. Mesmo com uma chamada recursiva para esse objeto global ou
undefined
essas funções aninhadas chamadas no pai
Também é uma situação importante!
const x = 0; function foo() { function bar(){ return this.x; } return bar(); } const obj = {x:10}; obj.test = foo; obj.test();
Isso ocorre porque a chamada para
bar()
equivalente à chamada para
LE_foo.bar
e o objeto do ambiente lexical é indefinido como esse.
Funções de construtor
Como escrevi acima:
isso nas funções é determinado pelo chamador e depende da sintaxe da chamada.
Invocamos funções de construtor usando a nova palavra-chave. A peculiaridade desse método de ativação da função é que o método da função interna
[[construct]] é chamado, que executa determinadas operações (o mecanismo para criar entidades pelos designers será discutido no segundo ou terceiro artigo sobre OOP!) E chama o método
[[call]] interno, que coloca
nesta instância criada da função construtora.
Cadeia de escopo
A cadeia de escopo também é uma propriedade do contexto de execução como este. É uma lista de objetos dos ambientes lexicais do contexto atual e todos os contextos geradores. É nessa cadeia que a pesquisa de variáveis ocorre ao resolver nomes de identificadores.
Nota: isso associa uma função a um contexto de execução e ScopeChain a contextos filhos.
A especificação afirma que ScopeChain é uma matriz:
SC = [LO, LO1, LO2,..., LOglobal];
No entanto, em algumas implementações, como JS, a cadeia de escopo é implementada por meio de
listas vinculadas .
Para entender melhor o ScopeChain, discutiremos o ciclo de vida das funções. É dividido na fase de criação e na fase de execução.
Quando uma função é criada, é atribuída a propriedade interna
[[SCOPE]] .
Em
[[SCOPE]] , uma cadeia hierárquica de objetos de ambientes lexicais de contextos mais altos (geradores) é registrada. Essa propriedade permanece inalterada até que a função seja destruída pelo coletor de lixo.
Preste atenção!
[[SCOPE]] , diferente do ScopeChain, é uma propriedade da própria função, não seu contexto.
Quando uma função é chamada, seu contexto de execução é inicializado e preenchido. O contexto é afixado com ScopeChain = LO (da própria função) + [[SCOPE]] (cadeia hierárquica de LO que afeta contextos).
Resolução dos nomes dos identificadores - pesquisa seqüencial de objetos
LO na cadeia
ScopeChain da esquerda para a direita. A saída é um ReferenceType cuja propriedade base aponta para o objeto LO no qual o identificador foi encontrado e PropertyName será uma representação em cadeia do nome do identificador.
É assim que o fechamento é organizado sob o capô! Um fechamento é essencialmente o resultado de uma pesquisa no ScopeChain por todas as variáveis cujos identificadores estão presentes na função.
const x = 10; function foo () { return x; } (function (){ const x = 20; foo();
O exemplo a seguir ilustra o ciclo de vida
[[SCOPE]] .
function foo () { const x = 10; const y = 20; return function () { return [x,y]; } } const x = 30; const bar = foo();
Uma exceção importante é
a função construtora . Para este tipo de função, [[SCOPE]] sempre aponta para um objeto global.
Além disso, não esqueça que se um dos links da cadeia ScopeChain tiver um protótipo, a pesquisa também será realizada no protótipo.
Conclusão
Apresentaremos as principais idéias de maneira tese:
- esse é o relacionamento da entidade com o contexto de execução
- ScopeChain é o relacionamento de uma entidade com todos os contextos de desova
- this e ScopeChain são propriedades do contexto de execução
- Esta função é determinada pelo chamador e depende da sintaxe da chamada.
- ScopeChain é o ambiente lexical do contexto atual + [[Scope]]
- [[Escopo]] - esta é uma propriedade da própria função, contém uma cadeia hierárquica de ambientes lexicais de contextos geradores
Espero que o artigo tenha sido útil. Até artigos futuros, amigos!