执行上下文、词法作用域和调用栈是什么关系?
先理解原理和示例,再练习完整口述与追问。
回答要点
作用域由定义位置决定;执行上下文承载一次执行的环境;调用栈组织同步调用;闭包延长词法环境中所需绑定的生命周期。
原理拆解
词法作用域描述代码可以访问哪些变量,由代码定义时的嵌套结构决定,不由调用位置决定。执行上下文是执行代码时建立的运行环境,包含词法环境、变量环境以及 this 等执行所需状态。全局代码开始执行会建立全局上下文,函数被调用时建立对应的函数上下文,执行完成后从调用栈退出。
例如在 make 函数中定义 read,read 访问 make 的局部变量;即使把 read 返回给外部调用,它查找变量时仍沿定义处的环境链。函数调用退出不等于这些绑定立即消失,如果返回函数仍引用它们,相关环境仍可能存活,这就是闭包的基础。
调用栈说明“现在执行到哪层调用”,作用域链说明“变量去哪里找”,二者不能混为一谈。Promise 回调和定时器回调在后续调度时才重新进入调用栈,不会把之前整个同步调用栈一直挂在那里。
示例与推演
外层函数先定义 x=1 并返回 read,外部再定义 x=9。read() 仍读到外层工厂创建的绑定,而不是调用方的 x;若同一外层环境又把 x 改成 2,read 会读到 2。调用栈已退出但环境仍被需要,这能同时解释词法作用域与闭包。
易错点
调用栈深度与作用域链长度不是一回事。调用别人的函数不会把自己的局部变量自动提供给它。
参考回答可直接练习口述
词法作用域描述代码可以访问哪些变量,由代码定义时的嵌套结构决定,不由调用位置决定。执行上下文是执行代码时建立的运行环境,包含词法环境、变量环境以及 this 等执行所需状态。全局代码开始执行会建立全局上下文,函数被调用时建立对应的函数上下文,执行完成后从调用栈退出。
面试官可能继续问追问与完整解答展开收起
追问
为什么 let 声明前访问报错,而 var 通常得到 undefined?
追问回答
创建绑定与执行赋值是不同阶段。var 绑定在相应环境初始化为 undefined,函数声明通常也会提前初始化;let、const 绑定创建后,在执行到声明初始化之前处于暂时性死区,访问会抛 ReferenceError。不能简单说 let 完全没有提升;const 也只是禁止重新绑定,引用对象的内部属性仍可能修改。