写ref="/tag/137/" style="color:#B2A89E;font-weight:bold;">JavaScript时,我们常会遇到变量找不到、函数执行顺序混乱的问题。比如你在调试一段嵌套调用的代码,明明定义了变量,却提示 undefined;或者某个函数执行完后,下一层函数突然拿不到前面的数据。这些问题背后,往往和“调用栈”与“作用域链”密切相关。
调用栈:函数是怎么一层层执行的
你可以把调用栈想象成一摞盘子。每当你调用一个函数,就往上面放一个盘子;函数执行完,就把最上面的盘子拿走。这就是“后进先出”的原则。
举个生活中的例子:你正在做饭,先烧水(函数A),烧水过程中发现要洗菜(函数B),洗菜时又想起得切姜(函数C)。这时候你的任务顺序就是 C → B → A。必须先把姜切完,才能继续洗菜;洗完菜,再回到烧水。如果切姜的步骤出错了,整个流程就会中断,错误信息也会指向当前最顶上的“盘子”。
在JavaScript中,这个“盘子”叫做执行上下文(Execution Context)。每当函数被调用,就会创建一个新的上下文,并推入调用栈。函数执行完毕后,上下文被弹出。
function first() {
second();
}
function second() {
third();
}
function third() {
console.log('当前在最顶层函数');
}
first(); // 调用栈:first → second → third → 执行输出 → 逐层退出
如果函数递归太深,比如自己调自己停不下来,调用栈就会溢出,浏览器报错 “Maximum call stack size exceeded”。就像那摞盘子堆太高,哗啦一下全倒了。
作用域链:变量从哪里找
作用域链解决的是“变量去哪儿找”的问题。每个函数在定义时,就已经决定了它能访问哪些变量。这个“权限名单”是在函数写出来的时候定下的,不是调用时才决定的。
比如你在家里找钥匙,先翻口袋,再查茶几,最后去玄关挂钩上看看。这个查找路径就是作用域链。JavaScript也一样:先看自己内部有没有定义,没有就往外层函数找,一直找到全局作用域为止。
var name = '小明';
function outer() {
var age = 18;
function inner() {
var hobby = '篮球';
console.log(name); // 小明 —— 全局层
console.log(age); // 18 —— 外层
console.log(hobby); // 篮球 —— 当前层
}
inner();
}
outer();
这里 inner 函数能访问到 name 和 age,是因为作用域链会沿着定义的位置逐层向上查找。但反过来不行,outer 无法访问 inner 里的 hobby,因为作用域是单向向外的。
有个容易混淆的点:函数在哪里调用,不影响它的作用域链。哪怕你把 inner 拿到外面去执行,它依然只能访问 outer 定义时的那个环境。
两者如何协作
当一个函数被调用时,JavaScript引擎会做两件事:一是把这个函数推入调用栈,二是根据它的定义位置建立作用域链,用来查找变量。
调用栈控制“执行顺序”,作用域链控制“数据访问”。它们一个管“谁在运行”,一个管“能拿到啥”。配合起来,构成了JavaScript函数执行的核心机制。
比如闭包现象,本质上就是函数带着自己的作用域链“出门”了。即使外层函数已经从调用栈里消失了,内部函数仍然保留着对那个环境的引用,所以还能访问原来的变量。
function counter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const inc = counter();
inc(); // 1
inc(); // 2
虽然 counter 已经执行完了,调用栈里早没了它,但返回的函数依然通过作用域链记着 count 这个变量。这就是为什么 count 能持续累加。