网站

JavaScript 作用域 - W3School

闭包 - JavaScript | MDN

引入

JS 作用域 里学过,变量有各自的有效范围。

那问题来了:如果当前作用域里找不到一个变量,JS 会怎么办?

答案是:顺着作用域链,一层一层往外找

正文

定义

定义

作用域链是 JS 查找变量时遵循的路径。从当前作用域开始,逐级向外层查找,直到全局作用域为止。

变量查找规则

当访问一个变量时,JS 的查找顺序:

  1. 先在当前作用域
  2. 找不到就去外层作用域
  3. 还找不到就继续往外,一直到全局作用域
  4. 全局作用域也找不到,就报 ReferenceError
let color = "红色";
 
function outer() {
  let color = "蓝色";
 
  function inner() {
    console.log(color);
  }
 
  inner();
}
 
outer();

输出 "蓝色"

inner() 自己没有 color,去外层 outer() 里找到了 "蓝色",就不再往外找了。

找到就停,不会继续往外找。

作用域嵌套

函数可以嵌套,作用域也跟着嵌套。

let a = 1;
 
function level1() {
  let b = 2;
 
  function level2() {
    let c = 3;
 
    function level3() {
      let d = 4;
      console.log(a, b, c, d);
    }
 
    level3();
  }
 
  level2();
}
 
level1();

输出:1 2 3 4

level3() 的作用域链:

level3 自身 → level2level1 → 全局

所以它可以访问到所有层级的变量。

作用域链的图

用一个简单的图来表示查找方向:

全局作用域          ← 最后查找
  └── level1 作用域  ← 第三层
        └── level2 作用域  ← 第二层
              └── level3 作用域  ← 最先查找

查找方向:从内往外,从下往上。

变量遮蔽

如果内层和外层有同名变量,内层的会遮蔽外层的。

let name = "全局";
 
function test() {
  let name = "函数";
 
  console.log(name);
}
 
test();

输出 "函数"

当前作用域有 name,就用自己的,不会去外面找。

这和找东西一样:手边有的就不会去翻柜子。

找不到变量的情况

function test() {
  console.log(x);
}
 
test();

报错:ReferenceError: x is not defined

JS 从 test() 内部找到全局,都没找到 x,就报错了。

多层函数实战

看看下面这段代码的输出:

let fruit = "苹果";
 
function kitchen() {
  let fruit = "香蕉";
 
  function fridge() {
    let fruit = "橘子";
 
    function eat() {
      console.log("吃了" + fruit);
    }
 
    eat();
  }
 
  fridge();
}
 
kitchen();

输出 "吃了橘子"

eat() 自己有 fruit,直接用自己的 "橘子"

如果去掉 eat() 里的 fruit

let fruit = "苹果";
 
function kitchen() {
  let fruit = "香蕉";
 
  function fridge() {
    let fruit = "橘子";
 
    function eat() {
      console.log("吃了" + fruit);
    }
 
    eat();
  }
 
  fridge();
}
 
kitchen();

还是 "吃了橘子",因为 fridge() 里有 fruit

再去掉 fridge() 里的 fruit

let fruit = "苹果";
 
function kitchen() {
  let fruit = "香蕉";
 
  function fridge() {
    function eat() {
      console.log("吃了" + fruit);
    }
 
    eat();
  }
 
  fridge();
}
 
kitchen();

输出 "吃了香蕉",找到了 kitchen() 里的 fruit

作用域链在函数定义时确定

作用域链是在函数定义时就确定好的,不是调用时。

let color = "红色";
 
function showColor() {
  console.log(color);
}
 
function changeAndCall() {
  let color = "蓝色";
  showColor();
}
 
changeAndCall();

输出 "红色"

虽然 showColor() 是在 changeAndCall() 里面被调用的,但它的作用域链是在定义时就固定了。

showColor 定义在全局作用域,所以它只认全局的 color

常见错误

误以为调用位置影响作用域链

let x = 10;
 
function a() {
  console.log(x);
}
 
function b() {
  let x = 20;
  a();
}
 
b();

输出 10,不是 20

a() 的作用域链是 a → 全局,跟 b 没有关系。

忘记变量被遮蔽

let count = 100;
 
function test() {
  let count = 0;
  count = count + 1;
  console.log(count);
}
 
test();
console.log(count);

输出:

1
100

函数内部的 count 遮蔽了全局的 count,修改的也是内部的。

特点

  • 变量查找从当前作用域开始,逐层向外
  • 找到就停止,找不到就报 ReferenceError
  • 内层变量会遮蔽外层同名变量
  • 作用域链在函数定义时确定,和调用位置无关
  • 嵌套越深,作用域链越长

理解

把作用域链理解成找东西时的习惯

你在自己书桌上找不到笔,就去客厅找。客厅没有,就去小区超市找。

找到一支就不找了,不会跑更远的地方。

如果到处都找不到,就说”找不到”(报错)。

作用域链就是这个查找路线:从最近的开始,一层一层往外翻,找到就停。

引出

理解了作用域链之后,可以继续学习 JS 闭包,看函数是怎么在作用域链上”记住”外层变量的。

也可以学习 JS 执行机制,了解代码在浏览器里到底是怎么一步步运行的。