我通过一段简单的helloworld程序,看看JavaScript代码加载、解析、执行流程

function f() {
    console.log("helloworld!");
}
f();

在JavaScript中,上面代码的的执行流程、解析过程以及内存区域的变化可以详细描述如下:

一、运行过程

1、加载:

当浏览器或JavaScript引擎(如V8引擎)遇到这段JavaScript代码时,它会首先将代码从磁盘或网络加载到内存中。

2、词法分析

加载完成后,JavaScript引擎会对加载过来的代码进行词法分析(tokenization),将源代码文本分解成一系列的词法单元,为后续的语法分析做好准备。

具体点说,就识别出变量名、函数名、操作符(+、-、*等)、分隔符(如逗号、分号)以及字面量(如数字、字符串)等。

3、语法分析

主要任务是将词法分析阶段产生的词法单元(tokens)组合成有意义的表达式、语句和代码块,从而构建出代表程序语法结构的抽象语法树(AST)。

具体点说,语法分析器会根据语言的语法规则来确定这些词法单元之间的关系和层次结构。确保它们符合JavaScript的语法规则。如果代码中存在语法错误,语法分析器会报告这些错误。

一旦语法分析完成并成功构建了抽象语法树(AST),这个树形结构就会作为后续编译或解释阶段的输入。后续的编译器或解释器会基于AST生成可执行代码或执行其他任务,如优化、代码生成等。

因此,语法分析是确保JavaScript代码语法正确性的关键步骤,它使得编译器或解释器能够理解和执行源代码中的结构和逻辑。

3、代码生成与优化

在生成AST后,引擎可能会将其转换成字节码(对于某些引擎,如V8),或直接转换成机器码。

为什么说是可能转换成字节码或机器码?
并不是所有的JavaScript引擎都会采用相同的策略。有些引擎可能直接将AST解释执行,而不进行字节码的转换。还有一些引擎可能采用其他形式的中间表示或即时编译(JIT)策略,以提高执行效率。
这个过程中还可能包含各种优化,比如隐藏类(hidden classes)和内联缓存(inline caching)。

4、函数与变量存储

函数f作为一个对象被存储在堆(Heap)内存中。这个对象包含指向函数代码的引用。

由于f是一个全局函数,它的引用也会被添加到全局对象(在浏览器中通常是window对象)的属性中。

5、执行函数

当调用f()时,JavaScript引擎会创建一个新的执行上下文,并将其推入调用栈(Call Stack)。

这个上下文包含f函数的局部变量、this值和任何传入的参数(尽管在这个例子中f没有参数)。

然后,引擎开始执行函数体内的代码。

6、执行console.log

在函数f内部,console.log("helloworld!")被调用。这会导致引擎执行console.log函数,并将字符串"helloworld!"作为参数传递。

console.log是宿主环境(如浏览器或Node.js)提供的一个函数,用于在控制台输出信息。它本身可能涉及一些内部对象的创建和内存分配,但这些通常对开发者是透明的。

7、执行上下文弹出

函数f执行完毕后,其执行上下文会从调用栈中弹出,释放分配的栈内存。

二、内存区域变化

1、代码区

包含函数f的源代码和console.log的实现代码。

2、堆(Heap)

  • 函数f作为一个对象被存储在堆内存中。
  • 如果console.log实现涉及动态分配的对象(比如用于存储日志信息的对象),这些对象也会被存储在堆上。

3、栈(Call Stack)

  • f被调用时,它的执行上下文(包括局部变量、this值和参数等)被推入调用栈。
  • 函数执行完毕后,执行上下文从栈中弹出,自动释放其占用的栈内存。

4、全局执行上下文

  • 包含全局变量和函数。在这个例子中,f作为全局函数存储在全局对象中。

注意事项

  • 上述描述是基于一个简化的JavaScript引擎模型。实际的JavaScript引擎(如V8、SpiderMonkey等)会有更多的优化和内部机制,但基本流程是相似的。
  • 内存管理在JavaScript中主要由垃圾回收器(Garbage Collector)自动处理。当没有变量引用某个对象时,该对象就成为垃圾回收的目标。
  • 开发者通常不需要关心内存管理的细节,但应该避免创建不必要的全局变量和内存泄漏,以确保程序的性能和稳定性。

附录C JavaScript中函数为什么是对象

对象可以拥有属性和方法。

深入理解JavaScript- JavaScript代码加载、解析、执行流程_开发语言


我们debug代码时,还没有执行到f时,JavaScript引擎已经为f生成了一个对象。

也就是说,JavaScript引擎在编译阶段,已经为全局作用域的函数生成了函数对象放入了堆内存。

⚠️注意:

  • 对于函数表达式,函数对象通常在执行到表达式时才创建。
  • 如果函数是在执行阶段动态创建的(例如,通过new Function()构造函数),那么它会在调用时创建并执行。

附录A 参考资料

资料1: JavaScript中的内存区域

附录B JavaScript是解释型语言,为什么还有编译

虽然JavaScript通常被视为一种解释型语言,但这并不意味着它没有编译的过程。实际上,现代的JavaScript引擎(如V8引擎)在执行JavaScript代码之前,会进行一个即时编译(JIT,Just-In-Time)或动态编译的过程。

这个过程与传统编译语言中的编译有所不同。在JavaScript中,代码不是提前编译成机器码,而是在运行时根据需要进行编译。
(在java中,所有的.java代码需要提前编译成.class文件,然后部署到服务器上)

当JavaScript引擎遇到一个函数或代码块时,它会将这部分代码编译成机器码或字节码,以提高后续的执行效率。这种编译是动态和即时的,可以根据程序的运行情况进行优化。

因此,尽管JavaScript是解释执行的,但它仍然具有编译的过程,只是这个过程是隐式的,并且在运行时进行。这种即时编译技术使得JavaScript能够在保持解释型语言灵活性的同时,获得更好的执行性能。