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

Does a browser truly read JavaScript line by line OR does it make multiple passes?

本文关键字:读取 JavaScript 逐行 浏览器      更新时间:2023-09-26

我明白JavaScript是解释而不是编译的。没问题。然而,我一直在这里读到JavaScript是"在飞行中"执行的,并且每次读取一行。当涉及到下面的例子时,这个想法让我很困惑:

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

需要说明的是,这段代码可以很好地写入控制台。但是,如果浏览器还没有到达这个函数,它怎么知道exampleFunction()的存在呢?

换句话说,这个函数到底是什么时候第一次解释的?

首先,您做了一个错误的假设:编译了现代JavaScript。V8、SpiderMonkey、Nitro等引擎将JS源代码编译成主机平台的本机机器码。

即使在旧的引擎中,JavaScript也不会被解释。它们将源代码转换成字节码,由引擎的虚拟机执行。

这实际上是Java和。net语言的工作方式:当你"编译"你的应用程序时,你实际上是将源代码分别转换为平台的字节码、Java字节码和CIL。然后在运行时,JIT编译器将字节码编译成机器码。

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

那么JS编译是如何工作的呢?在第一阶段,将源文本转换为抽象语法树(AST),这是一种数据结构,它以机器可以处理的格式表示代码。从概念上讲,这与如何将HTML文本转换为其DOM表示非常相似,这是您的代码实际处理的内容。

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

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

一旦引擎有AST:

  • 解释器可以直接从AST开始执行指令,这是非常慢的。
  • JS VM实现使用AST生成字节码,然后开始执行字节码。
  • 编译器使用AST生成机器代码,由CPU执行。
所以你现在应该可以看到至少, JS的执行分两个阶段。

然而,执行阶段实际上对示例的工作原理没有影响。它之所以有效,是因为定义了如何计算和执行JavaScript程序的规则。规则可以很容易地以一种方式编写,使您的示例无法工作,而不会影响引擎本身实际解释/编译源代码的方式。

具体来说,JavaScript有一个通常被称为提升的特性。为了理解提升,必须理解函数声明函数表达式之间的区别。

简单地说,函数声明就是声明一个将在其他地方调用的新函数:

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源代码实际上是在现代浏览器中编译的。
  • 引擎编译代码在多个通道
  • 行为是你的例子是JavaScript语言的规则的副产品,而不是如何编译或解释。

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

然而,

var foo = function(){};

将不检查,因此下面将抛出一个TypeError: undefined is not a function

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

它确实需要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()。第一次传递解析失败,不执行任何代码。

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

由于所有变量和函数声明都在当前作用域中(在您的例子中是全局脚本作用域)提升,因此您将能够调用下面声明的函数。

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

关键是,JavaScript将首先被编译器"读取",编译器存储已经定义为function foo(){...}的函数。只要从相同或从属作用域调用它们,就可以在脚本中的任何给定时间调用它们。现代编译器也做的是预分配对象,所以作为一个副作用,为了性能问题,强类型变量是有意义的。

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

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

  • 创建阶段

  • Excecution阶段

创建阶段

在创建阶段,我们有Global object, thisouter environment引用。在创建阶段,当解析器遍历代码并开始设置我们为翻译而编写的内容时,它会识别我们在哪里创建了变量以及在哪里创建了函数。它基本上为变量和函数设置了内存空间。

执行阶段

在这种情况下,代码逐行运行(在计算机上解释、转换、编译和执行)。

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

b();
function b() {
  console.log("I have been hoisted");
}