一、变量提升
js
// 变量声明与赋值
var myname = '竹合'; // 这个代码可以看成是两部分
// 声明 var myname; 赋值 myname = '竹合'
// 函数声明
function foo() {
console.log('foo');
} // 直接声明函数
var bar = function() {
console.log('bar');
} // 先声名变量var 再把函数赋值给变量bar
所谓的变量提升 ,是指JS代码执行过程中,JS引擎把变量的声明部分和函数的声明部分 提升到代码开头的行为,变量提升后,会给变量设置默认值undefined。
也就是函数和变量执行之前都提升到了代码开头(执行上下文开头)
1.var
声明的变量
js
console.log(x); // undefined
var x = 5;
console.log(x); // 5
尽管 console.log(x)
出现在 var x = 5;
语句之前,但不会引发错误。在编译阶段,var x;
的声明被提升到顶部,因此第一个 console.log(x)
输出 undefined
。 只有声明会被提升,而初始化不会。因此,变量 x
的值是在执行到 var x = 5;
时赋值的,第二个 console.log(x)
输出 5
。
2. 函数声明,整个函数都会被提升:
js
hello(); // "Hello, world!"
function hello() {
console.log("Hello, world!");
}
hello
函数在其声明之前被调用,这是因为整个函数声明都被提升到其所在作用域的顶部。
3. let
或 const
声明的变量不会被提升
js
console.log(y); // ReferenceError: y is not defined
let y = 10;
console.log(y); // 10
原因:
- Temporal Dead Zone (TDZ-暂时性死区): 使用
let
和const
声明的变量存在一个叫做 "Temporal Dead Zone"(时态死区)的概念。在声明变量之前的代码区域称为时态死区,在这个区域内访问变量会导致ReferenceError
。 - Block Scoping(块级作用域): 使用
let
和const
声明的变量具有块级作用域,而不是函数级作用域。这与var
不同,它具有函数级作用域。
js
if (true) {
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
}
y
的作用域仅限于 if
语句块内,而不是整个函数或全局作用域。在声明之前访问 y
会触发时态死区。
由于这些规则,JavaScript 引擎不会将 let
和 const
声明的变量提升到作用域的顶部。变量只有在代码执行到达声明语句时才会被初始化,因此在声明之前的代码中访问这些变量会导致时态死区错误。这样的设计使得 JavaScript 更具可预测性和安全性。
实际上变量和函数声明在代码里的位置是不会改变的,而且在编译阶段被JS引擎放入在内存中。 在JS的内存机制种讲解变量函数如何存储的。(暂时还没写 后续补上)
二、JS代码的执行流程
- 解析(Parsing):
JavaScript 引擎首先会对代码进行解析,将源代码转换为抽象语法树(Abstract Syntax Tree,AST)。
- 编译(Compilation):
解析后,引擎会对生成的 AST 进行编译,将其转换为可执行的字节码或机器代码。有些 JavaScript 引擎(比如 V8)会使用即时编译(Just-In-Time Compilation,JIT Compilation)将字节码直接转换为机器代码。
在这里得到了AST和执行上下文后,解释器就会根据AST生成字节码,并解释执行字节码。这里可能会有疑问,不是应该转为执行效率更高的机器码吗。一开始V8是把AST直接转为机器码的,让它获得了显著的性能提升;但随着移动设备的普及,特别是在手机端,内存空间有限,我们知道机器码其实就是一堆二进制数据,很占空间,所以导致V8的这种方式,极大的消耗内存。后来为了解决这个问题,V8引入了字节码。因为字节码体积比机器码的小得多,减少了系统内存的占用。
- 执行(Execution):
有了字节码后,接下来解释器就开始解释执行字节码了。在解释器执行字节码的过程中,如果发现有热点代码,就是某段代码被重复执行了多次,就称为热点代码,那么编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
代码开始执行。执行过程中,JavaScript 引擎按照特定的规则和步骤逐行执行代码。
在 V8 引擎中,Y编译器
指的是 TurboFan
,它是 V8 引擎的即时编译器(JIT Compiler)。TurboFan
负责将 JavaScript 代码转换为高效的本地机器代码。
V8 引擎中的编译器包括两个主要组件:
- Crankshaft(爆震短程): Crankshaft 是 V8 引擎早期版本中的编译器,负责将 JavaScript 代码编译成优化的中间表示(Intermediate Representation,IR)。然后,它通过优化和生成本地机器代码来执行。
- TurboFan(涡轮扇):
TurboFan
是 V8 引擎的后续版本中引入的编译器。它取代了Crankshaft
,并提供了更先进的优化技术。TurboFan
不仅负责生成本地机器代码,还能够执行更高级的优化,包括类型推断、内联缓存、多层次优化等。
TurboFan
使用了一种称为"Sea of Nodes"
的表示形式,这是一种图形表示法,用于表示中间表示中的操作和数据流。这种表示形式使得 TurboFan
能够进行更灵活、更智能的优化。
三、执行上下文
执行上下文(Execution Context)是 JavaScript
中管理代码执行的环境的抽象概念。每当 JavaScript
代码在运行时,都会创建一个执行上下文,用于管理变量、函数声明、this指向
等信息。
每个执行上下文都有自己的变量对象(Variable Object)、作用域链(Scope Chain)、this 指向
,以及一些其他信息。执行上下文可以分为三种类型:
1. 全局执行上下文(Global Execution Context):
是默认的、最外层的执行上下文。它在整个 JavaScript
程序的生命周期中一直存在,包含了全局变量和函数。
2. 函数执行上下文(Function Execution Context):
每当调用一个函数时,都会创建一个新的函数执行上下文。每个函数都有自己的执行上下文,包含了在函数内声明的局部变量和函数参数。
3. eval
函数执行上下文:
使用 eval 函数时创建的执行上下文,但由于不推荐使用 eval
,不做详细讨论,后面有说(this讲解中,还没写)。
执行上下文的生命周期包括两个阶段:
- 创建阶段(Creation Phase):
在这个阶段,JavaScript
引擎会创建变量对象、建立作用域链、确定 this
的值,并进行其他一些初始化工作。
- 代码执行阶段(Code Execution Phase):
在这个阶段,JavaScript
引擎会按照代码的顺序执行具体的语句,给变量赋值,执行函数等。
执行上下文的栈(Execution Context Stack)是一个栈数据结构,用于管理执行上下文的顺序。当执行一个函数时,会将该函数的执行上下文推入栈中,函数执行完毕后,其执行上下文会从栈中弹出,控制权交给下一个执行上下文。
总的来说,执行上下文是 JavaScript
运行时环境的一个关键概念,它确保了代码的正确执行并提供了作用域、变量和 this
的正确解析。
-
在JS执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生命周期内,全局执行上下文只有一份
-
当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束后,创建的函数执行上下文会被销毁。
-
当使用
eval
函数的时候,eval
的代码也会被编译,并创建执行上下文。