一、作用域:变量访问的规则边界
1.1 作用域的核心定义
作用域(Scope)是JavaScript中变量和函数的可访问范围,它决定了代码中标识符(变量名、函数名)的可见性和生命周期。JavaScript采用词法作用域(Lexical Scoping),即作用域在函数定义时确定,而非执行时。
示例1:词法作用域验证
function outer() {const outerVar = 'I am outside!';function inner() {console.log(outerVar); // 输出: 'I am outside!'}inner();}outer();
即使inner函数在outer函数内部执行时被调用,它仍能访问outerVar,因为作用域链在定义时已静态确定。
1.2 作用域的分类与特性
- 全局作用域:脚本最外层定义的变量,任何函数均可访问。
- 函数作用域:函数内部定义的变量,仅函数内可访问。
- 块级作用域(ES6新增):通过
let/const定义的变量,仅在代码块(如if、for)内有效。
示例2:块级作用域对比
// var无块级作用域if (true) {var varVar = 'var';}console.log(varVar); // 输出: 'var'// let有块级作用域if (true) {let letVar = 'let';}console.log(letVar); // 报错: letVar is not defined
1.3 面试题解析:作用域相关问题
问题1:以下代码输出什么?为什么?
for (var i = 0; i < 3; i++) {setTimeout(() => console.log(i), 100);}
答案:输出三个3。因为var无块级作用域,i在全局作用域中共享,循环结束时i为3。
改进方案:使用let或闭包。
// 使用letfor (let i = 0; i < 3; i++) {setTimeout(() => console.log(i), 100); // 输出: 0, 1, 2}// 使用闭包for (var i = 0; i < 3; i++) {(function(j) {setTimeout(() => console.log(j), 100);})(i); // 输出: 0, 1, 2}
二、作用域链:变量查找的路径
2.1 作用域链的构成
作用域链(Scope Chain)是函数执行时用于查找变量的链式结构,由当前函数作用域、外层函数作用域及全局作用域依次组成。
示例3:作用域链查找过程
const globalVar = 'Global';function outer() {const outerVar = 'Outer';function inner() {const innerVar = 'Inner';console.log(innerVar); // 1. 查找自身作用域console.log(outerVar); // 2. 查找外层作用域console.log(globalVar); // 3. 查找全局作用域}inner();}outer();
2.2 作用域链的优化与性能
- 避免过度嵌套:深层作用域链会增加变量查找时间。
- 缓存外层变量:将外层变量赋值给局部变量可减少查找。
示例4:作用域链优化
// 未优化function heavyCalculation() {const outerData = fetchData(); // 假设需从外层获取for (let i = 0; i < 1e6; i++) {// 每次循环都需沿作用域链查找outerDataprocess(outerData);}}// 优化后function heavyCalculation() {const outerData = fetchData();const localData = outerData; // 缓存到局部作用域for (let i = 0; i < 1e6; i++) {process(localData); // 直接访问局部变量}}
2.3 面试题解析:作用域链问题
问题2:以下代码输出什么?为什么?
const a = 1;function foo() {console.log(a); // 输出什么?}function bar() {const a = 2;foo();}bar();
答案:输出1。因为foo的作用域链不包含bar的作用域,它仅能访问自身和全局作用域。
三、闭包:作用域的持久化与封装
3.1 闭包的定义与本质
闭包(Closure)是指函数能够访问并记住其定义时的作用域,即使该函数在其定义的作用域之外执行。
示例5:闭包的基本形式
function createCounter() {let count = 0;return function() {count++;return count;};}const counter = createCounter();console.log(counter()); // 1console.log(counter()); // 2
counter函数记住了createCounter作用域中的count变量。
3.2 闭包的常见应用场景
- 数据封装:创建私有变量。
- 函数工厂:生成特定功能的函数。
- 模块模式:实现模块化开发。
示例6:模块模式实现
const module = (function() {const privateVar = 'Secret';function privateMethod() {console.log(privateVar);}return {publicMethod: function() {privateMethod();}};})();module.publicMethod(); // 输出: 'Secret'// module.privateMethod(); // 报错: 无法访问
3.3 闭包的内存管理与注意事项
- 内存泄漏:闭包会持久化外层变量,需手动释放无用闭包。
- 循环中的闭包:需注意变量共享问题。
示例7:循环中的闭包问题
// 错误示例for (var i = 1; i <= 3; i++) {setTimeout(function() {console.log('i:', i); // 输出三个3}, 100);}// 正确方案1:使用IIFEfor (var i = 1; i <= 3; i++) {(function(j) {setTimeout(function() {console.log('j:', j); // 输出: 1, 2, 3}, 100);})(i);}// 正确方案2:使用letfor (let i = 1; i <= 3; i++) {setTimeout(function() {console.log('i:', i); // 输出: 1, 2, 3}, 100);}
3.4 面试题解析:闭包问题
问题3:如何实现一个计数器,每次调用增加1,且计数器状态不被外部修改?
答案:使用闭包封装私有变量。
function createCounter() {let count = 0;return {increment: function() {count++;return count;},getCount: function() {return count;}};}const counter = createCounter();console.log(counter.increment()); // 1console.log(counter.increment()); // 2console.log(counter.getCount()); // 2// counter.count = 100; // 无效,count为私有变量
四、综合面试题解析
综合题:以下代码输出什么?为什么?如何修复?
const funcs = [];for (var i = 0; i < 3; i++) {funcs.push(function() {console.log(i);});}funcs.forEach(func => func());
答案:
- 输出:三个
3。因为var无块级作用域,所有函数共享同一个i。 - 修复方案:
- 使用
let:for (let i = 0; i < 3; i++) {funcs.push(function() {console.log(i);});}
- 使用闭包:
for (var i = 0; i < 3; i++) {(function(j) {funcs.push(function() {console.log(j);});})(i);}
- 使用
五、总结与建议
- 作用域:理解词法作用域,优先使用
let/const避免变量污染。 - 作用域链:减少嵌套深度,缓存外层变量提升性能。
- 闭包:合理利用封装特性,注意内存泄漏问题。
- 面试准备:熟练掌握作用域链查找顺序、闭包应用场景及循环变量问题。
实践建议:
- 编写代码时,明确变量作用域,避免意外覆盖。
- 使用闭包时,标注闭包持有的变量,便于维护。
- 通过实际项目练习闭包与模块化开发。