六维教程

JavaScript 作用域

写了这么多代码,有没有想过一个问题。为什么有的变量在哪里都能用,有的变量只能在函数内部用,还有的变量出了花括号就消失了。这些现象都和变量声明的位置有关。作用域就是变量可访问的范围,这篇把作用域彻底讲清楚,它是理解下一篇闭包的基础。

全局作用域

在函数和花括号块之外声明的变量属于全局作用域,任何地方都能访问,包括函数内部。

const siteName = '我的博客';

function showSite() {
  console.log(siteName); // 函数内也能访问
}

showSite();          // 输出 我的博客
console.log(siteName); // 输出 我的博客

siteName 声明在最外层,函数内函数外都能用。全局变量虽然方便,但建议少用。多个脚本都往全局放变量,名字很容易冲突,一个地方改错所有地方受影响,代码一多就难以维护。

函数作用域

函数内部声明的变量属于局部作用域,只在本函数内可访问,函数外面访问不到。

function greet() {
  const message = '你好';
  console.log(message); // 函数内可以
}

greet();
console.log(message); // 报错,message 未定义

函数执行完,内部的局部变量就被回收了。两个函数声明同名变量互不干扰,因为各自的变量只在自己的函数里。

function fn1() {
  const name = '小明';
  console.log(name);
}

function fn2() {
  const name = '小红';
  console.log(name);
}

fn1(); // 输出 小明
fn2(); // 输出 小红

块级作用域

let 和 const 声明的变量还有一个特性,只在最近的 {} 花括号内有效,这叫块级作用域。if 和 for 的花括号都是块。

if (true) {
  const num = 10;
  console.log(num); // 输出 10
}

console.log(num); // 报错,num 在块外不存在

for 的循环变量就是典型例子,循环结束 i 就消失了。

for (let i = 0; i < 3; i++) {
  console.log(i);
}

console.log(i); // 报错

这个特性让代码更安全,块内临时用的变量不会泄漏到外面。for 循环那篇提到的”每次迭代独立绑定”也是靠这个特性。变量声明到底用 let 还是 const,可以参考JavaScript 变量声明 let 与 const那篇。

变量查找从内到外

内层作用域使用变量时,先找自己这一层,找不到就往外层找,再找不到继续往外,直到全局作用域。找到就停止,不会回头找里层的同名变量。

const name = '全局的小明';

function outer() {
  const name = '函数的小明';

  function inner() {
    console.log(name); // 输出 函数的小明
  }

  inner();
}

outer();

inner 内部没有声明 name,往外找到 outer 里的 name,用它,不再继续往外找。如果 inner 自己声明了 name,就用自己那个,外层的更看不到。

作用域链的直观理解

把作用域想象成一层套一层的盒子。最外层是全局盒子,函数每嵌套一层就多一个盒子。变量查找就像从最内层盒子开始,一层层往外翻,找到就停,翻到全局还没有就报未定义的错误。

const a = 1;         // 全局盒子

function fn1() {     // 盒子 1,包住下面的内容
  const b = 2;

  function fn2() {   // 盒子 2,fn1 的子盒子
    const c = 3;
    console.log(a + b + c); // 内层可以访问外层的 a 和 b
  }
}

fn2 里能同时用 a、b、c,因为沿着盒子链从内到外都找得到。反过来外层盒子永远看不到内层盒子的变量。这条从内到外的链就叫作用域链,下一篇闭包的本质就和它有关。

声明前访问会报错

let 和 const 还有一个特点,声明之前不可访问。在声明语句之前使用变量会报错,这段时间称为暂时性死区。

console.log(num); // 报错,num 在声明前被使用
const num = 5;

把 const 换成 let 效果一样,都报错。但函数声明不一样,函数声明有提升机制,在定义之前调用也能正常工作。

greet(); // 输出 你好,定义之前调用也可以

function greet() {
  console.log('你好');
}

JavaScript 在运行前会把函数声明整体提升到作用域顶部,这个机制称为提升。变量的情况不一样,let 和 const 受暂时性死区限制,声明前使用就报错。所以实战中统一先声明再使用,养成好习惯,不必刻意依赖提升。

小结

  • 作用域是变量可访问的范围,全局作用域处处可访问,函数作用域只在函数内有效。
  • let 和 const 拥有块级作用域,只在最近的 {} 内有效,全局变量尽量少用。
  • 变量查找从内层到外层,找到即停,这条从内到外的链就是作用域链,可以想象成一层套一层的盒子。
  • let 和 const 声明前访问会报错,属于暂时性死区。函数声明有提升,定义之前调用也能工作。
上一篇
JavaScript 函数参数
下一篇
JavaScript 函数表达式与箭头函数