一、作用域:变量访问的规则边界
1.1 作用域的本质与分类
作用域(Scope)是JavaScript引擎管理变量可见性的核心机制,决定了代码中变量与函数的可访问范围。其本质是执行上下文(Execution Context)的静态属性,在代码解析阶段确定。
- 全局作用域:脚本最外层定义的变量,生命周期贯穿整个程序。
var globalVar = 'I am global';function checkScope() {console.log(globalVar); // 可访问}
- 函数作用域:通过
function关键字创建的封闭空间,内部变量对外不可见。function outer() {var localVar = 'Secret';console.log(localVar); // 可访问}outer();console.log(localVar); // ReferenceError
- 块级作用域(ES6+):
let/const声明的变量仅在代码块(如if、for)内有效。if (true) {let blockVar = 'Block scoped';}console.log(blockVar); // ReferenceError
1.2 变量提升与暂时性死区
- 变量提升:
var声明的变量会提升到作用域顶部(初始值为undefined),而函数声明会完整提升。console.log(foo); // undefinedvar foo = 'bar';function foo() {} // 函数声明覆盖变量
- 暂时性死区(TDZ):
let/const声明的变量在声明前访问会触发ReferenceError。console.log(baz); // ReferenceErrorlet baz = 'TDZ';
面试题:以下代码输出什么?为什么?
var a = 1;function b() {console.log(a); // undefinedvar a = 2;}b();
答案:输出undefined。函数b内部通过var声明了局部变量a,导致全局a被屏蔽,且局部a因变量提升在console.log时已存在但未赋值。
二、作用域链:变量查找的路径依赖
2.1 作用域链的构建机制
当函数被创建时,会保存其词法环境(Lexical Environment)的引用,形成作用域链。变量查找遵循“从内到外”的规则:
var outerVar = 'Outer';function outer() {var middleVar = 'Middle';function inner() {console.log(outerVar); // 查找链:inner → outer → 全局console.log(middleVar);}inner();}outer();
2.2 动态作用域的误区
JavaScript采用词法作用域(Lexical Scoping),即作用域链在函数定义时确定,而非调用时。这与动态作用域语言(如Bash)有本质区别。
面试题:以下代码输出什么?
var name = 'Global';function showName() {console.log(name);}function wrapper() {var name = 'Wrapper';showName();}wrapper();
答案:输出'Global'。showName的作用域链在定义时已固定,指向全局作用域。
三、闭包:跨越作用域的持久引用
3.1 闭包的定义与核心特性
闭包(Closure)是指函数能够访问并记住其定义时的作用域链,即使该函数在其词法作用域之外执行。
function createCounter() {let count = 0;return function() {count++; // 闭包保留了对count的引用return count;};}const counter = createCounter();console.log(counter()); // 1console.log(counter()); // 2
3.2 闭包的典型应用场景
- 数据封装与私有变量:
function createBankAccount(initialBalance) {let balance = initialBalance;return {deposit: (amount) => balance += amount,withdraw: (amount) => balance -= amount,getBalance: () => balance};}const account = createBankAccount(100);
- 事件处理与回调:
for (var i = 1; i <= 3; i++) {setTimeout(function() {console.log(i); // 总是输出4(var的坑)}, 1000);}// 修复方案:使用IIFE或letfor (let i = 1; i <= 3; i++) {setTimeout(() => console.log(i), 1000); // 正确输出1,2,3}
- 函数柯里化与高阶函数:
function multiply(a) {return function(b) {return a * b;};}const double = multiply(2);console.log(double(5)); // 10
3.3 闭包的内存管理
闭包会延长变量的生命周期,可能导致内存泄漏。需手动解除引用:
function heavyClosure() {const largeData = new Array(1e6).fill('data');return function() {console.log('Do something');};}const handler = heavyClosure();// 使用后解除引用handler = null;
面试题:以下代码存在什么问题?如何优化?
function setup() {const buttons = document.querySelectorAll('button');for (var i = 0; i < buttons.length; i++) {buttons[i].addEventListener('click', function() {console.log('Button ' + i + ' clicked');});}}
答案:所有按钮点击均输出'Button 3 clicked'(var导致循环变量共享)。优化方案:
- 使用
let替代var - 通过IIFE创建闭包:
for (var i = 0; i < buttons.length; i++) {(function(j) {buttons[j].addEventListener('click', function() {console.log('Button ' + j + ' clicked');});})(i);}
四、实战建议与调试技巧
- 严格模式(’use strict’):避免隐式全局变量,增强代码安全性。
- ESLint规则配置:启用
no-var、prefer-const等规则规范作用域使用。 - 开发者工具调试:
- Chrome DevTools的Scope面板可查看闭包变量。
- 使用
debugger语句或断点跟踪作用域链。
- 性能优化:
- 避免在循环中创建不必要的闭包。
- 对频繁调用的闭包函数进行缓存。
五、总结与面试准备
- 核心概念:作用域决定变量可见性,作用域链控制查找路径,闭包实现跨作用域访问。
- 高频考点:
- 词法作用域 vs 动态作用域
- 闭包的内存管理
let/var/const的作用域差异- 循环中的闭包问题
- 推荐练习:
- 手动模拟作用域链的创建过程。
- 实现一个支持私有成员的模块模式。
- 分析开源项目中的闭包使用案例。
通过系统掌握这些底层机制,开发者能够编写出更健壮、高效的代码,并在面试中展现出对JavaScript语言的深度理解。