JavaScript的作用域及其原理
七崽爱吃小饼干2026/01/20阅读 1专栏 JavaScript
一、作用域的核心概念
作用域(Scope)简单来说,就是变量和函数的可访问范围,它决定了代码中哪些部分可以访问某个变量/函数,以及变量的生命周期。
可以用生活中的例子理解:你家里的卧室(局部作用域)只有你能进,客厅(全局作用域)家里人都能进,作用域就是给变量/函数划定了“可访问的区域”,避免变量冲突、控制访问权限。
JS 中的作用域主要分为三类:
1. 全局作用域
- 定义:在代码任何地方都能访问的作用域,不属于任何函数/块的变量/函数都在全局作用域。
- 特点:
- 浏览器环境中,全局作用域的变量会挂载到
window对象上;Node.js 中挂载到global对象。 - 全局变量的生命周期贯穿整个程序,直到页面关闭/进程结束。
- 浏览器环境中,全局作用域的变量会挂载到
- 示例:
javascript
// 全局变量:在任何地方都能访问
let globalVar = "我是全局变量";
function test() {
console.log(globalVar); // 输出:我是全局变量
}
test();
console.log(window.globalVar); // 浏览器中输出:我是全局变量
2. 函数作用域(局部作用域)
- 定义:在函数内部声明的变量/函数,只能在函数内部访问,外部无法访问。
- 特点:
- 函数执行时创建作用域,执行结束后作用域销毁(除非有闭包)。
- 函数参数也属于函数作用域的变量。
- 示例:
javascript
function test() {
// 函数作用域变量:只能在test内部访问
let funcVar = "我是函数内的变量";
console.log(funcVar); // 输出:我是函数内的变量
}
test();
console.log(funcVar); // 报错:funcVar is not defined
3. 块级作用域(ES6 新增)
- 定义:由
{}包裹的区域(如if/for/while/switch或直接写{}),通过let/const声明的变量仅在该块内有效。 - 注意:
var没有块级作用域,只有函数作用域和全局作用域。 - 示例:
javascript
if (true) {
let blockVar = "我是块级变量"; // let声明,仅在if块内有效
var varVar = "var声明无块级作用域"; // var声明,全局可访问
}
console.log(blockVar); // 报错:blockVar is not defined
console.log(varVar); // 输出:var声明无块级作用域
二、作用域的底层原理
JS 引擎处理作用域的核心是 作用域链(Scope Chain) 和 词法环境(Lexical Environment),这两个概念决定了变量的查找规则。
1. 词法作用域(静态作用域)
JS 采用词法作用域(也叫静态作用域),即:变量的作用域在代码编写阶段就确定了,而非运行阶段。
- 关键规则:函数的作用域由它定义时的位置决定,而非调用时的位置。
- 示例:
javascript
let a = 1;
function fn1() {
console.log(a); // 这里的a指向全局的a,因为fn1定义在全局作用域
}
function fn2() {
let a = 2;
fn1(); // 调用fn1,但fn1的作用域由定义位置决定
}
fn2(); // 输出:1(而非2)
2. 词法环境(Lexical Environment)
词法环境是 JS 引擎的内部数据结构,用于存储变量和函数声明,每个作用域都对应一个词法环境,包含两部分:
- 环境记录(Environment Record):存储当前作用域的变量/函数(比如
let a = 1会存在这里)。 - 外部环境引用(Outer Environment Reference):指向父级作用域的词法环境(形成作用域链的关键)。
3. 作用域链(Scope Chain)
当 JS 引擎查找一个变量时,会遵循作用域链规则:
- 先在当前作用域的词法环境中查找变量;
- 如果没找到,就通过“外部环境引用”查找父级作用域;
- 依次向上查找,直到全局作用域;
- 如果全局作用域也没找到,就会报错(
xxx is not defined)。
作用域链示例(可视化):
javascript
// 全局作用域
let globalVar = "全局变量";
function outer() {
// outer作用域(父级:全局)
let outerVar = "outer变量";
function inner() {
// inner作用域(父级:outer)
let innerVar = "inner变量";
console.log(innerVar); // 自身作用域找到
console.log(outerVar); // 父级outer作用域找到
console.log(globalVar); // 父级的父级全局作用域找到
}
inner();
}
outer();
上述代码的作用域链查找路径:
inner 作用域 → outer 作用域 → 全局作用域
4. 变量提升(Hoisting)与作用域的关系
变量提升的本质是:JS 引擎在执行代码前,会先扫描当前作用域,将 var 声明的变量和函数声明“提升”到作用域顶部(但赋值操作留在原地)。
- 注意:
let/const也有提升,但会形成“暂时性死区(TDZ)”,在声明前访问会报错。 - 示例:
javascript
console.log(varVar); // 输出:undefined(var提升,未赋值)
var varVar = 1;
console.log(letVar); // 报错:Cannot access 'letVar' before initialization
let letVar = 2;
三、常见易混淆点
- 作用域 vs 执行上下文:
- 作用域:静态的,编写代码时确定,关注“变量可访问范围”;
- 执行上下文:动态的,函数执行时创建,关注“当前执行状态”(包含作用域链、this、变量对象等)。
- 闭包与作用域:闭包的本质是函数保留了对父级作用域的引用,即使父函数执行完毕,子函数仍能访问父函数的变量(突破了函数作用域的销毁规则)。
- 示例:
javascriptfunction outer() { let num = 10; return function inner() { console.log(num); // inner保留了outer作用域的引用,形成闭包 }; } const fn = outer(); fn(); // 输出:10(outer已执行完毕,但num仍能访问)
总结
- JS 作用域分为全局作用域、函数作用域、块级作用域(ES6),核心是控制变量的可访问范围;
- JS 采用词法作用域,变量作用域由定义位置决定,而非调用位置;
- 变量查找遵循作用域链规则:从当前作用域向上查找,直到全局作用域,底层依赖词法环境的“外部环境引用”实现。
掌握作用域的核心是理解“变量的访问范围由定义位置决定”和“作用域链的查找规则”,这也是后续学习闭包、this、模块化的基础。