浏览器是否真正逐行读取JavaScript还是多次通过?

问题描述 投票:18回答:6

我理解JavaScript是解释的而不是编译的。没问题。但是,我在这里继续阅读,JavaScript是“即时”执行的,并且每行读取一行。对于以下示例,这个想法让我感到困惑:

writeToConsole();

function writeToConsole() {
    console.log("This line was reached.");
}

为了记录,这段代码将写入控制台就好了。仍然,如果exampleFunction()尚未达到该功能,浏览器将如何知道它的存在?

换句话说,什么时候首先解释这个函数?

javascript browser interpreter
6个回答
47
投票

首先,你做了一个不正确的假设:编译现代JavaScript。像V8SpiderMonkey和Nitro这样的引擎将JS源代码编译到主机平台的本地machine code中。

即使在较旧的引擎中,JavaScript也不会被解释。他们将源代码转换为bytecode,引擎的virtual machine执行。

这实际上是Java和.NET语言中的工作方式:当您“编译”您的应用程序时,您实际上将源代码分别转换为平台的字节码Java bytecodeCIL。然后在运行时,JIT compiler将字节码编译成机器代码。

只有非常古老和简单的JS引擎实际上是interpret的JavaScript源代码,因为解释非常慢。

那么JS编译是如何工作的呢?在第一阶段,源文本被转换为abstract syntax tree (AST),这是一种数据结构,以机器可以处理的格式表示您的代码。从概念上讲,这很像HTML文本如何转换为DOM表示,这是您的代码实际使用的。

为了生成AST,引擎必须处理原始字节的输入。这通常由lexical analyzer完成。词法分析器并没有真正“逐行”读取文件;而是它逐字节读取,使用语言的语法规则将源文本转换为标记。然后词法分析器将令牌流传递给parser,这实际上构建了AST。解析器验证令牌是否形成有效序列。

您现在应该能够清楚地看到为什么语法错误会阻止您的代码完全正常工作。如果源文本中出现意外字符,则引擎无法生成完整的AST,并且无法继续进入下一阶段。

一旦引擎有AST:

  • 解释器可能只是直接从AST开始执行指令。这很慢。
  • JS VM实现使用AST生成字节码,然后开始执行字节码。
  • 编译器使用AST执行CPU的generate machine code

所以你现在应该能够看到,至少JS执行分两个阶段进行。

但是,执行阶段确实对您的示例工作原理没有影响。它的工作原理是rules定义了如何评估和执行JavaScript程序。规则可以很容易地编写,使得您的示例不起作用,而不会影响引擎本身实际解释/编译源代码的方式。

具体来说,JavaScript有a feature俗称吊装。为了理解提升,您必须了解函数声明和函数表达式之间的区别。

简单地说,函数声明就是当你声明一个将在别处调用的新函数时:

function foo() {

}

函数表达式是指在任何需要表达式的位置使用function关键字,例如变量赋值或参数:

var foo = function() { };

$.get('/something', function() { /* callback */ });

JavaScript要求在执行上下文的开头将函数声明(第一种类型)分配给变量名,而不管声明在源文本(上下文中)中出现的位置。执行上下文大致等同于范围 - 简单来说,函数内部的代码,或者如果不在函数内部,则是脚本的最顶层。

这可能导致非常奇怪的行为:

var foo = function() { console.log('bar'); };

function foo() { console.log('baz'); }

foo();

您希望将哪些内容记录到控制台?如果你只是线性地阅读代码,你可能会认为baz。然而,它实际上将记录bar,因为foo的声明悬挂在分配给foo的表达式之上。

总结如下:

  • JS源代码永远不会逐行“读取”。
  • JS源代码实际上是在现代浏览器中编译的(真正意义上的)。
  • Engines在多次传递中编译代码。
  • 行为是您的示例是JavaScript语言规则的副产品,而不是如何编译或解释。

9
投票

在执行任何代码之前,浏览器将首先检查所有函数。

然而,

var foo = function(){};

这将不会被检查,因此以下将抛出TypeError: undefined is not a function

foo();
var foo = function(){};

8
投票

确实需要2次通过。第一遍解析语法树,其中一部分正在执行提升。提升是使您发布的代码有效的原因。提升将任何var或命名函数声明function fn(){}(但不是函数表达式fn = function(){})移动到它们出现的函数的顶部。

第二遍执行解析,提升,并在一些引擎编译的源代码树。

看看这个例子。它显示了语法错误如何通过在第一次传递中抛出一个扳手来阻止脚本的所有执行,从而防止第二次传递(实际代码执行)发生。

var validCode = function() {
  alert('valid code ran!');
};
validCode();

// on purpose syntax error after valid code that could run
syntax(Error(

http://jsfiddle.net/Z86rj/

这里没有alert()。第一次传递解析失败,并且没有代码执行。


3
投票

首先解析脚本,然后解释,然后执行。执行第一个语句(writeToConsole();)时,已经解释了函数声明。

由于所有变量和function declarations都在当前范围(在您的情况下是全局脚本范围)中提升,因此您将能够调用下面声明的函数。


1
投票

JavaScript实际上是逐行解释的。但是,在执行之前,编译器会进行第一次传递,阅读某些内容(非常讨厌,看看这个:https://www.youtube.com/watch?v=UJPdhx5zTaw如果你真的很感兴趣)。

关键是,JavaScript将首先由编译器“读取”,编译器已经存储了定义为function foo(){...}的函数。您可以在脚本中的任何给定时间调用那些,因为您从相同或从属范围调用它们。现代编译器也做的是预分配对象,因此作为一个副作用,在性能问题上强烈键入变量是有意义的。

var foo = function(){...}将不会被编译器存储,因为JavaScript是松散类型的,并且变量的类型可能在执行期间发生变化。


0
投票

javascript引擎在执行代码之前创建执行上下文。执行上下文主要分两个阶段创建:

  • 创作阶段
  • 执行阶段

创作阶段

在创建阶段,我们有Global objectthisouter environment引用。在创建阶段,当解析器运行代码并开始设置我们为翻译编写的内容时,它识别我们创建变量的位置以及我们创建函数的位置。基本上它为变量和函数提升设置了存储空间。

执行阶段

在此,代码逐行运行(在计算机上解释,转换,编译和执行)。

考虑以下示例,在此我们甚至可以在声明之前调用函数b。这是因为javascript引擎已经知道函数b的存在。这也被称为javascript中的提升。

b();

function b() {
  console.log("I have been hoisted");
}
© www.soinside.com 2019 - 2024. All rights reserved.