this e ScopeChain no EcmaScript

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();//  10 ..    ReferenceType  base     obj const test = obj.foo;//       test();//  0 ..  test()   .test(),..  base    ,       0. 

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;// this     undefined    strict mode })() 

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(); //        //  ? (obj.foo)(); // ,    , GetValue   // ? (obj.foo = obj.foo)(); //        GetValue,     Fuction,   ReferenceType,   0   (   this) //  ||    ,    ..? (obj.foo || obj.foo)();// 0    ,     //  [obj.foo][0]();// 0    ,     // .. 

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();// undefined 

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();// 10 ..     <b><i>[[SCOPE]]</i></b> 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();//   ,   foo    bar();// [10,20] .. [[SCOPE]]    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!

Source: https://habr.com/ru/post/pt468943/


All Articles