在 JS 中, 当一段代码被执行时, JS 引擎会对其进行编译, 并创建 执行上下文。那么 哪些代码才会在执行前就进行编译 并创建 执行上下文:
1. 当 JS 执行全局代码的时候,会 编译全局代码 并创建 全局执行上下文,在整个页面的生存周期内,全局执行上下文只有一份。
2. 当调用一个函数的时候,函数体内的代码会被编译,并创建 函数执行上下文,一般情况下,函数执行结束后,创建的 函数执行上下文会被销毁。
3. 当使用 eval 函数的时候,eval 代码也会被编译,并创建 执行上下文。
那么 JS 如果管理这些这些执行上下文:通过一种栈的数据结构来管理。
一段简单的代码示例
01执行过程:
1. 编译阶段,创建 全局执行上下文,起中包含了变量环境: a = undefined 和 foo 函数;
2. 执行阶段,当执行 foo 函数的时候,JS 判断这是一个 函数调用,将执行下列操作:
1) 从全局执行上下文取出 foo 函数
2)对 foo 函数进行编译,并创建 foo 函数的执行上下文 和 执行代码
3) 执行代码,输出结果
当执行 foo 函数的时候,我们就有了两个 执行上下文: 全局的 和 foo 函数的。
栈
栈 是一种 后进先出 的数据结构, 就像垒积木, 当我们每次放置一块积木,这样的操作就叫 入栈,而每次取出最上面的那块的操作就叫 出栈。 只有将 栈内的 数据一层层 取出,我们才能拿到 最开始 进入的 数据。
JS 调用栈: JS 利用这种数据结果来管理 执行上下文,在创建好 执行上下文后,JS 引擎会将 执行上下文 压入栈中,通常把这种用来管理执行上下文的栈 称为 执行上下文栈,又叫 调用栈。
举个栗子
02分析代码
全局执行上下文11. 编译代码,创建全局执行上下文,将全局上下文 压入栈底。
2. 执行代码,将 b 赋值 为 1,执行 bar(2, 3) 函数,JS 引擎会编译该函数,创建函数的执行上下文并 将 该上下文压入栈中
bar 函数执行上下文1) 进入 bar 函数的执行阶段,首先 将 c 赋值为 2,接着 执行 foo() 函数,为其 创建 foo 函数的 执行上下文, 并压入栈顶
foo 函数执行上下文2) foo 函数执行阶段,返回结果 设置 result 的值,执行结束后, foo 函数的执行上下文销毁,从栈顶弹出
弹出foo函数执行上下文3)接着 执行 bar 函数的 执行代码, 返回结果, bar 函数执行结束,弹出 执行上下文
弹出bar函数执行上下文调用栈只剩全局执行上下文,JS 代码执行结束,整个流程也就结束了。
调用栈是JavaScript引擎追踪函数执⾏的⼀个机制 ,当⼀次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。
调用栈 在 JS 调试过程的作用
当我们在调试 JS 代码的时候,往往会加入断点,通过该断点, 我们就可以查看改函数的调用栈了,一步步分析每个 函数执行上下文,查找到 bug 所在。
vs code 调试代码左侧是调用栈的数据,跟上面图片中的结构一致;我们也可以通过 console.trace() 来打印出当前的栈内数据情况。
栈溢出
调用栈是有大小限制的,当超出一定的数目,JS 就会报错,就是所谓的 栈溢出。
最常见到的就是 当写递归代码的时候,却没有写 退出条件。
JS 引擎开始执行这段代码时,首先调⽤函数add, 并创建执⾏上下⽂,压⼊栈中;这个函数是递归的,没有终止的条件,所以它会⼀直创建新的 函数执行上下文,并反复将其压⼊栈中,栈是有大小限制的,超过最大数量后就会出现栈溢出的错误。
所以 我们在写递归的时候一定要注意 退出的条件, 或者将递归改成其他形式的,比如 for 循环。
总结
1. 每调用⼀个函数,JS 引擎会为其创建 执行上下文,并把该执行上下文压⼊调用栈,接着执行函数内的代码。
2. 如果在⼀个函数A中调用了另外⼀个函数B,那么JS 引擎会为B函数创建执行上下⽂,并将B函数 的执行上下文压入栈顶。
3. 当前函数执行完毕后,JS 引擎会将该函数的执行上下文弹出栈。
4. 当调用栈空间被占满时,会引发“堆栈溢出”问题。
网友评论