JavaScript 的作用域(Scope)和作用域链(Scope Chain)
本文阐述JavaScript的作用域与作用域链。作用域是决定变量可访问范围的规则(分全局、函数、块级),作用域链则是变量由内向外逐级查找的机制。
我们来深入浅出地讲解一下 JavaScript 中非常核心的概念:作用域 (Scope) 和 作用域链 (Scope Chain)。
理解这两个概念对于编写高质量、无 bug 的 JavaScript 代码至关重要。
一、作用域 (Scope)
1. 什么是作用域?
你可以把作用域想象成一套规则,这套规则用来管理和查找变量。简单来说,作用域定义了变量和函数在代码中哪些区域是可访问的。
它的主要目的有两个:
- 隔离变量:防止不同代码块中的变量互相冲突(即“命名冲突”)。
- 安全性:决定了代码块对外的暴露程度,有些变量只允许内部使用,外部无法访问。
2. 作用域的类型
在 JavaScript 中,主要有三种类型的作用域:
a. 全局作用域 (Global Scope)
- 定义:在代码的最外层定义的变量,或者在所有函数之外定义的变量,都拥有全局作用域。
- 特点:
- 在代码的任何地方都可以被访问和修改。
- 在浏览器环境中,全局作用域的变量会自动成为
window对象的属性。 - 滥用全局变量容易导致命名冲突和代码耦合度高,应尽量避免。
示例:
var globalVar = "我是一个全局变量";
function checkGlobal() {
console.log(globalVar); //可以访问
}
checkGlobal(); // 输出: "我是一个全局变量"
console.log(window.globalVar); // 在浏览器中,输出: "我是一个全局变量"
b. 函数作用域 (Function Scope)
- 定义:在函数内部声明的变量,只在该函数及其嵌套的子函数内部可以访问。
- 特点:
- 这是由
var关键字声明变量时遵循的规则。 - 函数外部无法访问函数内部的变量。
- 这是由
示例:
function myFunction() {
var functionVar = "我是一个函数作用域变量";
console.log(functionVar); // 可以访问
}
myFunction(); // 输出: "我是一个函数作用域变量"
// console.log(functionVar); // Uncaught ReferenceError: functionVar is not defined
c. 块级作用域 (Block Scope)
- 定义:在
{}(花括号) 代码块中声明的变量,只在该代码块内部可以访问。 - 特点:
- 这是 ES6 引入的
let和const关键字所遵循的规则。 if语句、for循环、while循环等都会创建块级作用域。- 它解决了
var在循环等场景下可能引发的经典问题。
- 这是 ES6 引入的
示例:
if (true) {
let blockVar = "我是一个块级作用域变量";
const blockConst = "我也是";
console.log(blockVar); // 输出: "我是一个块级作用域变量"
}
// console.log(blockVar); // Uncaught ReferenceError: blockVar is not defined
var vs let in for loop (经典对比):
使用 var:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 循环结束后,i 的值已经是 3,所以会输出三次 3
}, 100);
}
// 输出: 3, 3, 3
这是因为 var 是函数作用域,三次循环共享同一个变量 i。
使用 let:
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // let 是块级作用域,每次循环都会创建一个新的 i
}, 100);
}
// 输出: 0, 1, 2
二、词法作用域 (Lexical Scope)
在讲解作用域链之前,必须先理解一个概念:词法作用域(也叫静态作用域)。
JavaScript 采用的是词法作用域。这意味着,函数的作用域在函数定义的时候就已经决定了,而不是在函数调用的时候。
简单来说:无论一个函数在哪里被调用,它的作用域规则只由它被声明时所处的位置决定。
示例:
var value = "global";
function foo() {
console.log(value); // 这个 value 指向哪里?
}
function bar() {
var value = "local";
foo(); // 在 bar 内部调用 foo
}
bar(); // 输出: "global"
分析:虽然 foo 是在 bar 内部被调用的,但 foo 是在全局作用域中定义的。根据词法作用域的规则,它在查找 value 变量时,会沿着它定义时的作用域链去查找,也就是在它自己的作用域(为空)和全局作用域中查找,所以找到了全局的 "global"。
三、作用域链 (Scope Chain)
1. 什么是作用域链?
当代码在一个作用域中查找一个变量时,如果当前作用域中找不到,它就会向上一层作用域继续查找,这个逐层向上查找的过程就形成了一个链条,这就是作用域链。
- 起点:当前执行代码所在的作用域。
- 终点:全局作用域。
可以把作用域链想象成一个建筑物的楼层:
- 当前作用域:你所在的房间。
- 外部作用域:你房间外的楼层。
- 全局作用域:整栋大楼的一楼大厅。
你要找一个东西(变量),会先在自己房间找,找不到就去楼层里找,再找不到就去一楼大厅找。如果一楼大厅也找不到,那就说明这个东西(变量)不存在(ReferenceError)。
2. 作用域链是如何工作的?
让我们通过一个嵌套函数的例子来具体看看:
// 全局作用域
var a = 1;
function outer() {
// outer 函数作用域
var b = 2;
function inner() {
// inner 函数作用域
var c = 3;
console.log(a + b + c); // 在这里访问 a, b, c
}
inner();
}
outer(); // 输出: 6
当 inner 函数执行 console.log(a + b + c) 时,查找变量的作用域链如下:
查找
c:- 首先在
inner函数的当前作用域中查找。 - 找到了!
c的值是 3。
- 首先在
查找
b:- 首先在
inner函数的当前作用域中查找。没找到。 - 沿着作用域链向上,进入
outer函数的作用域。 - 找到了!
b的值是 2。
- 首先在
查找
a:- 首先在
inner函数的当前作用域中查找。没找到。 - 向上进入
outer函数的作用域。还是没找到。 - 再向上,进入全局作用域。
- 找到了!
a的值是 1。
- 首先在
这个查找路径 inner Scope -> outer Scope -> Global Scope 就是作用域链。
作用域链与闭包 (Closure)
作用域链是理解闭包的关键。闭包是指一个函数能够“记住”并访问其词法作用域(即定义时的作用域),即使该函数在其词法作用域之外执行。
function createCounter() {
let count = 0;
return function increment() {
count++;
console.log(count);
};
}
const counterA = createCounter(); // createCounter 执行完毕,其作用域本应销毁
counterA(); // 输出: 1
counterA(); // 输出: 2
// 为什么 counterA 能够记住 count 的值?
// 因为 increment 函数形成了一个闭包。
// 它的作用域链中包含了 createCounter 的作用域,所以它始终可以访问并修改变量 count。
总结
- 作用域 (Scope):是一套管理变量访问权限的规则。分为全局作用域、函数作用域和块级作用域。
- 词法作用域 (Lexical Scope):JavaScript 的作用域规则是静态的,在代码写下时就已确定,与函数如何被调用无关。
- 作用域链 (Scope Chain):是一个变量的查找机制。当需要访问一个变量时,解释器会从当前作用域开始,沿着链条逐级向上查找,直到全局作用域。
var,let,const的区别:var遵循函数作用域。let和const遵循块级作用域,提供了更精细的控制,是现代 JavaScript 的首选。