Vamos começar com uma pergunta simples:
<script type="text/javascript">
alerta(eu); // ?
var eu = 1;
</script>
O resultado de saída é indefinido. Este fenômeno é chamado de "pré-análise": o mecanismo JavaScript analisará primeiro as variáveis var e as definições de função. O código não é executado até que a pré-análise seja concluída. Se um fluxo de documentos contiver vários segmentos de código de script (código js separado por tags de script ou arquivos js importados), a ordem de execução será:
etapa 1. Leia o primeiro segmento de código.
etapa 2. Faça a análise de sintaxe. Se houver um erro, um erro de sintaxe será relatado (como colchetes incompatíveis, etc.) e pule para a etapa 5.
etapa 3. Faça a "pré-análise" das definições de variáveis e funções var (nenhum erro será relatado, porque apenas as declarações corretas são analisadas)
etapa 4. Execute o segmento de código e relate um erro se houver um erro (por exemplo, a variável é indefinida)
etapa 5. Se houver outro segmento de código, leia o próximo segmento de código e repita a etapa 2.
passo 6. Ao final da análise acima, ele conseguiu explicar muitos problemas, mas sempre sinto que falta alguma coisa. Por exemplo, na etapa 3, o que exatamente é “pré-análise”? E na etapa 4, observe o seguinte exemplo:
<script type="text/javascript">
alert(i); // erro: i não está definido.
eu = 1;
</script>
Por que a primeira frase causa um erro? Em JavaScript, as variáveis não precisam ser indefinidas?
O tempo do processo de compilação passou como um cavalo branco, e eu abri os “Princípios de Compilação” ao lado da estante como se estivesse a um mundo de distância. Havia esta nota no espaço em branco familiar, mas desconhecido:
Para linguagens compiladas tradicionais. , as etapas de compilação são divididas em: análise lexical e análise sintática, verificação semântica, otimização de código e geração de bytes.
Mas para linguagens interpretadas, depois que a árvore sintática é obtida por meio da análise lexical e da análise sintática, a interpretação e a execução podem começar.
Simplificando, a análise lexical consiste em converter um fluxo de caracteres (fluxo de caracteres) em um fluxo de token (fluxo de token), como a conversão de c = a - b para:
NOME "c";
IGUAIS
NOME "a"
MENOS
NOME "b"
PONTO E VÍRGULA
Os itens acima são apenas exemplos. Para obter mais informações, consulte Análise Lexical.
O Capítulo 2 de "O Guia Definitivo para JavaScript" fala sobre Estrutura Lexical, que também é descrita em ECMA-262. A estrutura lexical é a base de uma linguagem e é fácil de dominar. Quanto à implementação da análise lexical, esta é outra área de investigação e não será explorada aqui.
Podemos usar a analogia da linguagem natural. A análise lexical é uma tradução difícil de um para um. Por exemplo, se um parágrafo em inglês for traduzido para o chinês palavra por palavra, o que obtemos é um monte de fluxos de tokens, o que é difícil. entender. A tradução adicional requer análise gramatical. A figura a seguir é uma árvore sintática de uma instrução condicional:
Ao construir a árvore de sintaxe, se for descoberto que ela não pode ser construída, como if(a { i = 2; }, um erro de sintaxe será relatado e a análise de todo o bloco de código será encerrada. Esta é a etapa 2 em no início deste artigo.
Por meio da análise sintática, construa Após a árvore sintática, a frase traduzida ainda pode ser ambígua e é necessária verificação semântica adicional. Para linguagens tradicionais fortemente tipadas, a parte principal da verificação semântica é a verificação de tipo, como a verificação de tipo. parâmetros reais das funções e se os tipos de parâmetros formais correspondem. Para linguagens de tipo fraco, esta etapa pode não estar disponível (tenho energia limitada e não tenho tempo para examinar a implementação do mecanismo JS, por isso não tenho certeza se existe uma. etapa de verificação semântica no mecanismo JS)
. Acontece que, para mecanismos JavaScript, deve haver análise lexical e análise de sintaxe e, em seguida, pode haver etapas como verificação semântica e otimização de código após a conclusão dessas etapas de compilação (qualquer linguagem). um processo de compilação, mas as linguagens interpretadas não são compiladas em código binário), o código começará a ser executado.
O processo de compilação acima ainda não consegue explicar a "pré-análise" no início do artigo. processo do código JavaScript.
Zhou Aimin disse em "A Essência da Linguagem JavaScript".
A segunda parte da "Prática de Programação" tem uma análise muito cuidadosa disso.
traduzido em uma árvore de sintaxe e, em seguida, será executado imediatamente de acordo com a árvore de sintaxe,
o que requer execução adicional. Entenda o mecanismo de escopo do JavaScript. Em termos leigos, o escopo das variáveis JavaScript é determinado quando elas são definidas. em vez de quando são executados. Ou seja, o escopo léxico depende do código-fonte. O compilador pode determiná-lo por meio de análise estática, portanto, o escopo léxico também é chamado de escopo estático. e eval não pode ser realizado apenas por meio de tecnologia estática, só podemos falar sobre o mecanismo de escopo do JS.
O contexto de execução contém um contexto de execução. objeto de chamada. O objeto de chamada é uma estrutura scriptObject usada para salvar a tabela de variáveis internas, como varDecls, tabela de funções incorporadas funDecls e upvalue da lista de referências pai (nota: informações como varDecls e funDecls são obtidas durante o estágio de análise de sintaxe e são salvos na árvore de sintaxe Quando a instância da função é executada, essas informações serão copiadas da árvore de sintaxe para scriptObject). scriptObject é um sistema estático relacionado à função, consistente com o ciclo de vida da instância da função. .
O escopo lexical é o mecanismo de escopo do JS, e você também precisa entender seu método de implementação. Esta é a cadeia de escopo. A cadeia de escopo é um mecanismo de pesquisa de nome. Ele primeiro procura o scriptObject no ambiente de execução atual. Se não for encontrado, ele segue o upvalue até o scriptObject pai e procura o objeto global.
Quando uma instância de função é executada, um encerramento é criado ou associado a ela. scriptObject é usado para salvar estaticamente tabelas de variáveis relacionadas a funções, enquanto o encerramento salva dinamicamente essas tabelas de variáveis e seus valores em execução durante a execução. O ciclo de vida de um encerramento pode ser maior que o de uma instância de função. A instância da função será destruída automaticamente depois que a referência ativa ficar vazia, e o fechamento será reciclado pelo mecanismo JS depois que a referência de dados ficar vazia (em alguns casos, ela não será reciclada automaticamente, resultando em um vazamento de memória).
Não se deixe intimidar pelo monte de substantivos acima. Depois de entender os conceitos de ambiente de execução, objeto de chamada, fechamento, escopo léxico e cadeia de escopo, muitos fenômenos na linguagem JS podem ser facilmente resolvidos.
Resumo Neste ponto, as questões do início do artigo podem ser explicadas de forma muito clara:
A chamada "pré-análise" na etapa 3 é realmente concluída no estágio de análise de sintaxe da etapa 2 e armazenada na árvore sintática. Quando uma instância de função é executada, varDelcs e funcDecls serão copiados da árvore sintática para o scriptObject do ambiente de execução.
Na etapa 4, variáveis indefinidas significam que não podem ser encontradas na tabela de variáveis do scriptObject. O mecanismo JS pesquisará para cima ao longo do valor superior do scriptObject. Se nenhum for encontrado, a operação de gravação i = 1 será finalmente equivalente a window. i = 1; adiciona um novo atributo ao objeto janela. Para operações de leitura, se o scriptObject rastreado até o ambiente de execução global não puder ser encontrado, ocorrerá um erro de tempo de execução.
Após a compreensão, a neblina se dissipou e as flores desabrocharam, e o céu ficou claro.
Por fim, deixo-vos com uma pergunta:
<script type="text/javascript">
vararg = 1;
função foo(arg) {
alerta(argumento);
vararg = 2;
}
foo(3);
</script>
Qual é a saída do alerta?