Skip to content

JavaScript 作用域与作用域链详解

一、 作用域 (Scope)

1. 什么是作用域?

作用域(Scope)是在运行时代码中,某些特定部分中变量、函数和对象的可访问性。通俗来讲,作用域决定了代码区块中变量和资源的可见性

可以将其理解为一种隔离机制,其最大的作用就是隔离变量,使得不同作用域下的同名变量不会产生冲突。

javascript
// 作用域 A:全局作用域
let i = 1;

function a() {
  // 作用域 B:函数 a 的作用域
  let i = 2; // 与全局的 i 不冲突
  console.log(i); // 输出 2
}

a();

console.log(i); // 输出 1

// 在函数 a 外部无法访问其内部的变量
// console.log(i_inside_a); // Uncaught ReferenceError: i_inside_a is not defined

2. 作用域的类型

  • ES6 之前: 只有 全局作用域函数作用域
  • ES6 及之后: 新增了 块级作用域,主要通过 letconst 关键字实现。

二、 作用域的分类详解

1. 全局作用域 (Global Scope)

在代码中任何地方都能访问到的对象拥有全局作用域。

拥有全局作用域的情形:

1. 最外层函数和最外层函数外部定义的变量

javascript
// outVariable 在最外层,属于全局作用域
var outVariable = "外部变量";

function outFunc() {
  // inVariable 在函数内部,属于函数作用域
  var inVariable = "内部变量";

  function innerFunc() {
    // innerFunc 可以访问外层的 outVariable
    console.log(outVariable); // 输出 "外部变量"
    // innerFunc 也可以访问其父级作用域的 inVariable
    console.log(inVariable); // 输出 "内部变量"
  }
  innerFunc();
}

console.log(outVariable); // 输出 "外部变量"
outFunc();

// 在全局作用域无法访问函数作用域的变量
// console.log(inVariable); // ReferenceError: inVariable is not defined

2. 所有未定义直接赋值的变量(隐式全局变量)

在函数内部,如果一个变量未经声明(没有 var, let, const)就直接赋值,它会自动成为一个全局变量。(注意: 在严格模式下会报错)。

javascript
function test() {
  // i 没有被声明,直接赋值,成为全局变量
  i = 10;
}

test();
console.log(i); // 输出 10

3. 所有 window 对象的属性

在浏览器环境中,全局对象是 window,其所有属性都拥有全局作用域。

javascript
// 'location' 是 window 的一个属性
console.log(location); // 输出 Location 对象
// 这等价于
console.log(window.location);

全局作用域的弊端

污染全局命名空间,容易引起变量名冲突。

如果多个库或者多个开发者都向全局作用域添加了同名的变量,后引入的变量会覆盖先前的,造成程序错误。

示例:

javascript
// 张三写的代码
var data = { a: 1, b: 2 };

// 李四写的代码,如果和张三的代码在同一个项目里
var data = ['apple', 'banana'];

// 此时,张三的 data 对象被完全覆盖了
console.log(data); // 输出 ['apple', 'banana']

这就是为什么像 jQuery 等库会把所有代码放在一个立即执行函数(IIFE) 中,只向外暴露一个唯一的全局变量(如 $jQuery),以避免污染全局命名空间。

2. 函数作用域 (Function Scope)

在函数内部定义的变量,只能在该函数内部被访问,外部无法访问。

javascript
function doSomething() {
    var stuName = "Alice";
    console.log(stuName); // 输出 "Alice"

    function innerFunc() {
        // ...
    }
}

doSomething();

// 在函数外部访问,会报错
// console.log(stuName); // ReferenceError: stuName is not defined

3. 块级作用域 (Block Scope)

ES6 引入了 letconst 关键字,它们使得 JavaScript 拥有了块级作用域。一个 { } (花括号) 包裹的区域就是一个块级作用域。

在 ES5 中,if 语句和 for 语句的 {} 并不会创建新的作用域。

妙用:解决循环中的异步问题

一个经典的面试题是,在循环中为元素绑定事件。

错误示例 (var)

var 没有块级作用域,所有事件回调函数共享同一个变量 i。当事件被触发时,循环早已结束,此时 i 的值恒为循环结束后的值(3)。

html
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
javascript
var buttons = document.querySelectorAll('button');
for (var i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function() {
        // 点击任何按钮,都输出 "这是第 4 个按钮"
        // 因为共享的 i 在点击时已经是 3
        console.log('这是第 ' + (i + 1) + ' 个按钮');
    };
}

正确示例 (let)

let 会为循环的每一次迭代创建一个新的块级作用域,每个作用域中都有一个独立的 i 变量。

javascript
const buttons = document.querySelectorAll('button');
for (let i = 0; i < buttons.length; i++) {
    buttons[i].onclick = function() {
        // 点击按钮1,输出 "这是第 1 个按钮"
        // 点击按钮2,输出 "这是第 2 个按钮"
        // ...
        console.log('这是第 ' + (i + 1) + ' 个按钮');
    };
}

三、 作用域链 (Scope Chain)

1. 自由变量 (Free Variable)

定义: 在当前作用域中没有定义,但被使用了的变量。

javascript
const a = 100;

function fn() {
    // 在 fn 的作用域中,没有定义 a
    // 所以这里的 a 就是一个“自由变量”
    console.log(a);
}

fn(); // 输出 100

2. 什么是作用域链?

当一个作用域需要访问一个自由变量时,它会向外层(父级)作用域一层一层地去寻找,直到找到全局作用域为止。如果到全局作用域还找不到,就会报错。

这个一层一层向上寻找的链式关系,就是作用域链

javascript
let i = 10;

function a() {
    function b() {
        function c() {
            // 在 c 的作用域中,i 是自由变量
            // 1. 在 c 中找,没有
            // 2. 顺着链条去 b 中找,没有
            // 3. 顺着链条去 a 中找,没有
            // 4. 顺着链条去全局作用域找,找到了!值为 10
            console.log(i);
        }
        c();
    }
    b();
}
a(); // 输出 10

3. 关键知识点:词法作用域 (Lexical Scope)

重要: 函数的作用域链在函数创建(定义)时就已经确定,而不是在函数调用(执行)时确定。这被称为词法作用域静态作用域

函数在哪里定义,它的上级作用域就在哪里,和它在哪里被调用毫无关系。

示例 1:

javascript
let x = 10;

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

function show() {
  let x = 20;
  fn(); // fn 在这里被调用
}

show(); // 输出什么?

答案是 10

解析:

  1. fn 函数是在全局作用域中被创建的。
  2. 因此,它的作用域链是 fn 自己的作用域 -> 全局作用域
  3. fn 内部需要查找自由变量 x 时,它会沿着这个固定的链条去全局作用域找,找到了 x = 10
  4. 它并不会因为在 show 函数内部被调用,就去 show 的作用域里找 x

示例 2:

javascript
const food = 'rice';

function eat() {
  console.log(`eat ${food}`);
}

(function () {
  const food = 'noodle';
  eat(); // 在一个新作用域里调用 eat
})();

答案是 eat rice

解析: eat 函数在全局作用域创建,它内部 food 变量的查找路径在创建时就锁定了全局作用域。

如果想输出 eat noodle 呢? 需要将 eat 函数的创建位置也移到立即执行函数内部。

javascript
(function () {
  const food = 'noodle';
  
  // 在这里创建 eat 函数
  function eat() {
    console.log(`eat ${food}`);
  }

  eat(); // 输出 eat noodle
})();

四、 作用域 vs. 执行上下文

这是一个非常容易混淆的概念,但它们的本质区别在于静态动态

特性作用域 (Scope)执行上下文 (Execution Context)
性质静态的 (Static)动态的 (Dynamic)
确定时机在代码编写/定义时就已经确定,此后不再改变。在代码执行/调用时才创建,每次调用都可能不同。
核心决定了变量和函数的可访问性查找规则(作用域链)。管理代码执行的环境,最典型的就是 this 的指向。

总结:

  • 作用域是关于“查找规则”的一套静态规则,在引擎解析代码时就已定下。
  • 执行上下文是关于“当前执行环境”的一个动态概念,在函数被调用时才产生。
javascript
var x = 10;

var obj = {
  x: 20,
  fn: function() {
    console.log(this.x);
  }
};

obj.fn(); // 输出 20, this 指向 obj (由执行上下文决定)

var bar = obj.fn;
bar(); // 输出 10 (在浏览器非严格模式下), this 指向 window (执行上下文改变了)

// 尽管两次调用的 this 不同(执行上下文不同),
// 但 fn 函数的作用域始终是固定的:fn 自己的作用域 -> 全局作用域。

五、 面试题解答思路

谈谈你对作用域和作用域链的理解。

  1. 回答什么是作用域:

    • 先给出定义:作用域是定义变量的区域,它决定了变量的可访问性。
    • 阐述其核心作用:隔离变量,防止命名冲突。
    • 分点介绍作用域的类型:
      • 全局作用域:特点和弊端(全局污染)。
      • 函数作用域:函数内可访问,外部不可访问。
      • 块级作用域:ES6 let/const 带来的新特性,并举例说明其解决了什么问题(如循环绑定事件)。
  2. 回答什么是作用域链:

    • 先引入自由变量的概念。
    • 解释当访问自由变量时,JS 引擎会沿着一个链条从内到外地查找,这个链条就是作用域链
    • (关键点) 强调作用域是静态的/词法作用域。一个函数的作用域链在函数定义时就已确定,与它在哪里被调用无关。可以举例说明。
    • (加分项)可以简单区分一下作用域执行上下文的区别,突出自己理解的深度(静态 vs 动态)。