一、作用域与作用域链
1. 作用域的基础概念
作用域 决定了变量、函数和对象在代码中的 可见性 和 生命周期,它通过定义变量的“作用范围”,控制哪些代码可以访问特定的标识符(变量、函数等)。
简单来说就是:一个变量能够被看到,被拿来用的一个范围。
举个例子:
let a = "我是变量1"
function foo() {
let b = "我是变量2";
console.log(b);
}
foo(); // 调用foo函数,输出“ 我是变量2 ”
console.log(a); // 输出 "我是变量1"
console.log(b); // 报错:ReferenceError(无法访问函数作用域变量)
解析:
这段代码中,因为a是用 let 声明在全局作用域(脚本最外层),所以,和它同一层的 console.log(a) 在函数能够正常输出。
再看b,它因为声明在函数foo()中,所以在外面访问不到出现报错,只有通过调用foo()函数才能对b进行输出。
2. 作用域链(Scope Chain)
作用域链 是 JavaScript 引擎查找变量的路径,它遵循 “先当前,再向外” 的规则,在查找变量时执行流程如下:
- 当前执行上下文:查找变量时,首先它在当前函数或块级作用域中查找。
- 外部作用域链:如果未找到,则沿着作用域链向外部作用域逐层查找。
- 全局作用域:如果所有作用域均未找到变量,则抛出错误
ReferenceError。
举个例子:
let a = "我是全局变量a"
function outer() {
let b = "我是out里的变量b";
inner();
function inner(){
let c = "我是inner里的变量c";
console.log(a); // 我是全局变量a
console.log(i); // 报错:ReferenceError(无法访问函数作用域变量)
}
}
outer();
解析:
在上面代码中,在查找变量a时,它先是访问inner函数,当inner函数中未找到,它便会向外一层outer里面查找,就这样一层一层向外,直到在全局作用域中发现a
所以,它的作用域链便是:inner -> outer -> global
二、作用域的类型
1. 全局作用域(Global Scope)
定义:
全局作用域 是最外层的作用域,其变量或函数在程序的任何地方都可访问,在浏览器环境中,全局作用域的变量会成为 window 对象的属性。
特点:
-
变量声明:未使用
var、let或const声明的变量默认属于全局作用域。 -
生命周期:全局变量在程序运行期间始终存在,直到页面关闭或程序结束。
-
污染风险:过度使用全局变量可能导致命名冲突和内存泄漏。
示例:
let globalVar = "我是全局变量";
function funcA() {
console.log("我是全局函数 funcA");
}
function funcB() {
console.log(globalVar);
funcA();
}
funcB(); // 我是全局变量 我是全局函数 funA
解析:
在这段代码中,在全局作用域中定义了全局变量globalVar和全局函数funcA(),它们都可以被任何其他函数调用。
2. 函数作用域(Function Scope)
定义:
函数作用域 是函数内部定义的变量或函数的作用域,仅在该函数内部有效。使用 var 声明的变量属于函数作用域,无论其在 函数内部 哪个位置声明。
特点:
- 变量提升 :
var声明的变量会被提升到函数顶部,但赋值不会提升。 - 嵌套规则:内部函数可以访问外部函数的变量,但外部函数无法访问内部函数的变量。
- 作用域限制:函数作用域内的变量在函数外部不可见。
示例:
function outer() {
var a = "我是outer函数作用域变量 a";
function inner() {
var b = "我是inner函数作用域变量 b";
console.log(a); // 输出 "我是outer函数作用域变量 a"
}
inner(); // 调用内部函数
console.log(b); //报错:ReferenceError(外部无法访问内部变量)
}
console.log(a); // 报错:ReferenceError(函数作用域变量外部不可见)
解析:
-
函数作用域变量
a- 使用
var声明的变量a,仅在outer()函数内部有效。 - 内部函数
inner()可以访问a(嵌套规则),但outer()函数外部无法访问。
- 使用
-
变量提升(Hoisting)
- 如果
a在函数中未声明直接使用,JavaScript 会将其提升到函数顶部。
- 如果
-
嵌套规则
- 内部函数
inner()可以访问外部函数outer()的变量a。 - 但外部函数
outer()无法访问内部函数inner()的变量b。
- 内部函数
-
作用域限制
a和b都是函数作用域变量,在函数外部无法访问,因此console.log(a)和console.log(b)都会报错。
3. 块级作用域(Block Scope)
定义:
块级作用域 是由 {} 定义的代码块(如 if、for、while 等)内的作用域,使用 let 和 const 声明的变量属于块级作用域,而 var 声明的变量不具有块级作用域。
特点:
- 块级绑定:
let和const声明的变量仅在代码块内有效。 - 暂时性死区(TDZ) :
let和const声明的变量在声明前访问会抛出错误,而var会返回undefined。 - 避免变量提升:
let和const不会像var一样提升到代码块顶部。
示例:
function foo(){
console.log(funcVar); // 输出: undefined(TDZ)
if (true) {
console.log(blockVar); // 报错:ReferenceError(TDZ)
let blockVar = "我是块级作用域变量";
const blockConst = "我是块级常量";
var funcVar = "我是函数作用域变量";
console.log(blockVar); // 输出: 我是块级作用域变量
}
console.log(funcVar); // 输出: 我是函数作用域变量
console.log(blockVar); // 报错:ReferenceError(块级作用域变量外部不可见)
}
console.log(blockVar); // 报错:ReferenceError(块级作用域变量外部不可见)
解析:
-
块级作用域变量
blockVar和blockConst- 使用
let和const声明,该变量/常量仅在if代码块内有效。 if代码块外部无法访问blockVar。
- 使用
-
var的非块级行为funcVar使用var声明,属于函数作用域,因此在if代码块外部可访问。- 这也是
var与let/const的关键区别。
-
避免变量提升
let/const声明的变量不会被提升到代码块顶部,因此在声明前访问会触发错误。
4. 词法作用域(Lexical Scope)
定义:
词法作用域 也称静态作用域,是 JavaScript 的默认作用域规则,变量的作用域由代码中函数声明的位置决定,而非调用位置,词法作用域在代码编译阶段就已确定,因此被称为“静态作用域”。
特点:
- 作用域链:变量查找路径遵循“当前作用域 → 外部作用域 → 全局作用域”的顺序。
- 闭包基础:词法作用域是闭包实现的前提条件,内部函数可以访问外部函数的变量。
示例:
function outer() {
var outerVar = "我是外部函数变量";
function inner() {
console.log(outerVar); // 输出 "我是外部函数变量"
}
inner(); // 调用内部函数 inner()
}
outer(); // 输出 "我是外部函数变量"
解析:
-
词法作用域的定义
outerVar是在outer函数中声明的变量,inner函数在其内部定义,因此inner可以访问outerVar,即使outerVar的声明在inner之前。- 这种嵌套关系在代码编写时就已经确定,与函数调用位置无关。
-
作用域链规则
-
inner函数在查找outerVar时,遵循以下路径:- 首先检查
inner函数的作用域(无outerVar)。 - 然后向上查找
outer函数的作用域(找到outerVar)。 - 最终输出
outerVar的值。
- 首先检查
-
-
闭包的实现基础
inner函数即使在outer函数执行结束后,仍能访问outerVar,这是闭包的体现。- 闭包依赖于词法作用域的特性:内部函数始终能访问外部函数的作用域链。
-
静态 vs 动态作用域
- 与词法作用域相对的是动态作用域(如部分 Shell 脚本语言),变量作用域由函数调用位置决定。
- JavaScript 采用词法作用域,确保代码可预测性和稳定性。
作用域的对比与关系
| 作用域类型 | 声明方式 | 变量提升 | 作用域范围 | 生命周期 |
|---|---|---|---|---|
| 全局作用域 | var、不声明 | 是 | 整个代码 | 程序运行期间 |
| 函数作用域 | var | 是 | 函数内部 | 函数执行期间 |
| 块级作用域 | let、const | 否 | 代码块(如 if、for) | 代码块执行期间 |
| 词法作用域 | 静态规则 | - | 由函数定义位置决定 | 依赖闭包和作用域链 |
三、词法作用域与闭包:静态绑定与动态引用
1. 闭包的本质:词法作用域的动态绑定
闭包(Closure)是函数与其 词法作用域的绑定关系。其核心原理是:
- 函数在定义时捕获其外部作用域的变量,即使外部函数已执行完毕,这些变量依然被保留。
- 词法作用域决定了闭包的“查找路径” :函数内部变量的查找优先级始终遵循代码中声明的位置。
示例:闭包的形成
function outer() {
const secret = "密码"; // 外部函数的局部变量
return function inner() { // 内部函数引用外部变量
console.log(secret);
};
}
const closure = outer(); // outer执行完毕
closure(); // 输出 "密码"(secret未被释放)
解析:
-
inner是outer的内部函数,它引用了outer的变量secret。 -
outer执行后,其作用域按理应被销毁,但由于inner仍持有secret的引用,JavaScript 引擎会保留outer的作用域链,形成闭包,就像是outer离开前留给inner的一个小背包,里面有outer留下的一些属性。
2. 闭包的底层机制:作用域链与垃圾回收
(1)作用域链的持久化
- 每个函数在创建时都会生成一个 作用域链(Scope Chain) ,包含所有外部作用域的引用。
- 当函数返回时,如果其内部函数仍存在对变量的引用,作用域链不会被销毁,而是作为闭包的一部分保留在内存中。
(2)内存管理与垃圾回收
-
闭包变量不会被回收:只要闭包函数或其引用的变量未被释放,外部作用域的变量会一直驻留内存。
-
潜在风险:过度使用闭包可能导致内存泄漏,例如:
function createLargeArray() { const data = new Array(1000000).fill("数据"); return function () { console.log(data[0]); }; } const closure = createLargeArray(); // data始终占用内存
3. 闭包的核心应用场景
(1)数据封装与私有状态
通过闭包实现模块的私有变量,防止外部直接修改内部状态:
function createCounter() {
let count = 0; // 私有变量
return {
increment: () => count++,
decrement: () => count--,
getValue: () => count
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 1
// 无法直接访问 count
(2)回调函数与异步操作
闭包常用于异步回调中,绑定外部作用域的变量:
function createGreeting(name) {
return function (message) {
console.log(`Hello, ${name}! ${message}`);
};
}
const greetAlice = createGreeting("Alice");
setTimeout(greetAlice, 1000, "How are you?");
// 1秒后输出 "Hello, Alice! How are you?"
(3)函数工厂与柯里化
闭包可以动态生成函数,绑定特定参数:
function multiplyBy(factor) {
return function (number) {
return number * factor;
};
}
const double = multiplyBy(2);
console.log(double(5)); // 10
const triple = multiplyBy(3);
console.log(triple(5)); // 15
4. 闭包的注意事项与陷阱
(1)this 的绑定问题
闭包函数中的 this 取决于调用方式,而非定义时的作用域:
const obj = {
name: "Alice",
logName: function () {
setTimeout(function () {
console.log(this.name); // undefined(this指向window)
}, 100);
}
};
obj.logName();
解决方案:
-
使用箭头函数继承外部
this:logName: function () { setTimeout(() => { console.log(this.name); // "Alice" }, 100); } -
或显式绑定
this:setTimeout(function () { console.log(this.name); }.bind(this), 100);
(2)避免内存泄漏
-
及时释放无用闭包:当闭包不再需要时,将其赋值为
null,解除引用,允许垃圾回收:const closure = createLargeArray(); closure = null; // 释放闭包
五、总结
JavaScript 的作用域和变量提升机制是理解高级编程的关键。通过词法作用域规则,JavaScript 引擎能够高效地查找变量;通过变量提升,开发者需要理解声明与赋值的时间差;通过闭包,函数可以动态绑定其定义时的环境,实现数据封装和状态持久化。