JavaScript 作用域与作用域链详解
一、 作用域 (Scope)
1. 什么是作用域?
作用域(Scope)是在运行时代码中,某些特定部分中变量、函数和对象的可访问性。通俗来讲,作用域决定了代码区块中变量和资源的可见性。
可以将其理解为一种隔离机制,其最大的作用就是隔离变量,使得不同作用域下的同名变量不会产生冲突。
// 作用域 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 defined2. 作用域的类型
- ES6 之前: 只有
全局作用域和函数作用域。 - ES6 及之后: 新增了
块级作用域,主要通过let和const关键字实现。
二、 作用域的分类详解
1. 全局作用域 (Global Scope)
在代码中任何地方都能访问到的对象拥有全局作用域。
拥有全局作用域的情形:
1. 最外层函数和最外层函数外部定义的变量
// 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 defined2. 所有未定义直接赋值的变量(隐式全局变量)
在函数内部,如果一个变量未经声明(没有 var, let, const)就直接赋值,它会自动成为一个全局变量。(注意: 在严格模式下会报错)。
function test() {
// i 没有被声明,直接赋值,成为全局变量
i = 10;
}
test();
console.log(i); // 输出 103. 所有 window 对象的属性
在浏览器环境中,全局对象是 window,其所有属性都拥有全局作用域。
// 'location' 是 window 的一个属性
console.log(location); // 输出 Location 对象
// 这等价于
console.log(window.location);全局作用域的弊端
污染全局命名空间,容易引起变量名冲突。
如果多个库或者多个开发者都向全局作用域添加了同名的变量,后引入的变量会覆盖先前的,造成程序错误。
示例:
// 张三写的代码
var data = { a: 1, b: 2 };
// 李四写的代码,如果和张三的代码在同一个项目里
var data = ['apple', 'banana'];
// 此时,张三的 data 对象被完全覆盖了
console.log(data); // 输出 ['apple', 'banana']这就是为什么像 jQuery 等库会把所有代码放在一个立即执行函数(IIFE) 中,只向外暴露一个唯一的全局变量(如
$或jQuery),以避免污染全局命名空间。
2. 函数作用域 (Function Scope)
在函数内部定义的变量,只能在该函数内部被访问,外部无法访问。
function doSomething() {
var stuName = "Alice";
console.log(stuName); // 输出 "Alice"
function innerFunc() {
// ...
}
}
doSomething();
// 在函数外部访问,会报错
// console.log(stuName); // ReferenceError: stuName is not defined3. 块级作用域 (Block Scope)
ES6 引入了 let 和 const 关键字,它们使得 JavaScript 拥有了块级作用域。一个 { } (花括号) 包裹的区域就是一个块级作用域。
在 ES5 中,
if语句和for语句的{}并不会创建新的作用域。
妙用:解决循环中的异步问题
一个经典的面试题是,在循环中为元素绑定事件。
错误示例 (var)
var 没有块级作用域,所有事件回调函数共享同一个变量 i。当事件被触发时,循环早已结束,此时 i 的值恒为循环结束后的值(3)。
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>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 变量。
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)
定义: 在当前作用域中没有定义,但被使用了的变量。
const a = 100;
function fn() {
// 在 fn 的作用域中,没有定义 a
// 所以这里的 a 就是一个“自由变量”
console.log(a);
}
fn(); // 输出 1002. 什么是作用域链?
当一个作用域需要访问一个自由变量时,它会向外层(父级)作用域一层一层地去寻找,直到找到全局作用域为止。如果到全局作用域还找不到,就会报错。
这个一层一层向上寻找的链式关系,就是作用域链。
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(); // 输出 103. 关键知识点:词法作用域 (Lexical Scope)
重要: 函数的作用域链在函数创建(定义)时就已经确定,而不是在函数调用(执行)时确定。这被称为词法作用域或静态作用域。
函数在哪里定义,它的上级作用域就在哪里,和它在哪里被调用毫无关系。
示例 1:
let x = 10;
function fn() {
console.log(x);
}
function show() {
let x = 20;
fn(); // fn 在这里被调用
}
show(); // 输出什么?答案是 10。
解析:
fn函数是在全局作用域中被创建的。- 因此,它的作用域链是
fn 自己的作用域 -> 全局作用域。 - 当
fn内部需要查找自由变量x时,它会沿着这个固定的链条去全局作用域找,找到了x = 10。 - 它并不会因为在
show函数内部被调用,就去show的作用域里找x。
示例 2:
const food = 'rice';
function eat() {
console.log(`eat ${food}`);
}
(function () {
const food = 'noodle';
eat(); // 在一个新作用域里调用 eat
})();答案是 eat rice。
解析: eat 函数在全局作用域创建,它内部 food 变量的查找路径在创建时就锁定了全局作用域。
如果想输出 eat noodle 呢? 需要将 eat 函数的创建位置也移到立即执行函数内部。
(function () {
const food = 'noodle';
// 在这里创建 eat 函数
function eat() {
console.log(`eat ${food}`);
}
eat(); // 输出 eat noodle
})();四、 作用域 vs. 执行上下文
这是一个非常容易混淆的概念,但它们的本质区别在于静态与动态。
| 特性 | 作用域 (Scope) | 执行上下文 (Execution Context) |
|---|---|---|
| 性质 | 静态的 (Static) | 动态的 (Dynamic) |
| 确定时机 | 在代码编写/定义时就已经确定,此后不再改变。 | 在代码执行/调用时才创建,每次调用都可能不同。 |
| 核心 | 决定了变量和函数的可访问性和查找规则(作用域链)。 | 管理代码执行的环境,最典型的就是 this 的指向。 |
总结:
- 作用域是关于“查找规则”的一套静态规则,在引擎解析代码时就已定下。
- 执行上下文是关于“当前执行环境”的一个动态概念,在函数被调用时才产生。
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 自己的作用域 -> 全局作用域。五、 面试题解答思路
谈谈你对作用域和作用域链的理解。
回答什么是作用域:
- 先给出定义:作用域是定义变量的区域,它决定了变量的可访问性。
- 阐述其核心作用:隔离变量,防止命名冲突。
- 分点介绍作用域的类型:
- 全局作用域:特点和弊端(全局污染)。
- 函数作用域:函数内可访问,外部不可访问。
- 块级作用域:ES6
let/const带来的新特性,并举例说明其解决了什么问题(如循环绑定事件)。
回答什么是作用域链:
- 先引入自由变量的概念。
- 解释当访问自由变量时,JS 引擎会沿着一个链条从内到外地查找,这个链条就是作用域链。
- (关键点) 强调作用域是静态的/词法作用域。一个函数的作用域链在函数定义时就已确定,与它在哪里被调用无关。可以举例说明。
- (加分项)可以简单区分一下作用域和执行上下文的区别,突出自己理解的深度(静态 vs 动态)。