一、作用域:变量访问的规则边界
1.1 作用域的核心定义
作用域(Scope)是JavaScript中变量和函数可访问的上下文范围,决定了标识符(变量名、函数名)的可见性和生命周期。它像一道隐形的边界,将代码划分为不同的访问区域。
关键特性:
- 静态性:ES6前的作用域(函数作用域)在代码编写阶段确定,ES6的块级作用域(
let/const)在词法分析阶段确定。 - 层级性:作用域可嵌套,形成作用域链。
- 隔离性:内层作用域可访问外层变量,反之则不行(除非通过闭包)。
1.2 作用域的分类与代码示例
1.2.1 全局作用域
任何不在函数或代码块中的变量都属于全局作用域,可通过window对象(浏览器)或global对象(Node.js)访问。
var globalVar = "I'm global";function checkGlobal() {console.log(globalVar); // 输出: I'm global}checkGlobal();console.log(window.globalVar); // 浏览器中输出: I'm global
风险点:全局变量易被污染,导致命名冲突。
1.2.2 函数作用域
通过function声明的变量仅在函数内部有效。
function outer() {var funcVar = "Function scope";console.log(funcVar); // 输出: Function scope}outer();console.log(funcVar); // 报错: funcVar is not defined
1.2.3 块级作用域(ES6+)
let和const声明的变量仅在代码块({}、循环、条件语句)内有效。
if (true) {let blockVar = "Block scope";const constVar = "Constant";console.log(blockVar); // 输出: Block scope}console.log(blockVar); // 报错: blockVar is not defined
面试题:var与let/const在作用域上的区别?
var存在变量提升,无块级作用域;let/const有块级作用域,且存在暂时性死区(TDZ)。
二、作用域链:变量查找的路径
2.1 作用域链的形成机制
当函数被创建时,会保存其所在的作用域链(即定义时的词法环境)。函数执行时,若内部未找到变量,会沿作用域链向上查找,直至全局作用域。
示例:
var outerVar = "Outer";function outer() {var innerVar = "Inner";function inner() {console.log(outerVar); // 输出: Outer(沿作用域链向上查找)console.log(innerVar); // 输出: Inner(当前作用域)}inner();}outer();
2.2 作用域链的查找规则
- 从内到外:先查找当前作用域,再逐层向外。
- 就近原则:优先使用最近定义的变量。
- 静态绑定:作用域链在函数定义时确定,与调用位置无关(词法作用域)。
面试题:以下代码的输出是什么?
var a = 1;function foo() {console.log(a); // 输出: undefined(变量提升)var a = 2;}foo();
- 解析:
var a在函数内声明并提升,但赋值发生在console.log之后,故输出undefined。
三、闭包:作用域的持久化
3.1 闭包的定义与本质
闭包(Closure)是指函数能够访问并记住其定义时的作用域,即使该函数在其定义的作用域之外执行。其本质是函数与其词法环境的组合。
核心条件:
- 内部函数引用了外部函数的变量。
- 外部函数执行完毕后,其作用域未被销毁(因内部函数仍持有引用)。
3.2 闭包的典型应用场景
3.2.1 数据封装与私有变量
function createCounter() {let count = 0;return {increment: () => ++count,getCount: () => count};}const counter = createCounter();counter.increment();console.log(counter.getCount()); // 输出: 1console.log(count); // 报错: count is not defined(外部无法访问)
3.2.2 函数柯里化
function multiply(a) {return function(b) {return a * b;};}const double = multiply(2);console.log(double(5)); // 输出: 10
3.2.3 事件回调与异步操作
function setupClick(id) {const element = document.getElementById(id);element.addEventListener('click', () => {console.log(`Clicked ${id}`); // 闭包保留id的引用});}setupClick('btn');
3.3 闭包的性能与内存管理
- 优点:实现数据隐藏、状态保持。
- 缺点:可能导致内存泄漏(如未清理的DOM事件监听器)。
- 优化建议:
- 及时解除闭包对无用变量的引用(如设为
null)。 - 避免在循环中创建闭包(可用
let或IIFE解决)。
- 及时解除闭包对无用变量的引用(如设为
面试题:以下代码的输出及问题?
for (var i = 1; i <= 3; i++) {setTimeout(() => console.log(i), 100);}// 输出: 4 4 4(var无块级作用域,所有回调共享同一个i)
- 修复方案:使用
let或IIFE。// 方案1:letfor (let i = 1; i <= 3; i++) {setTimeout(() => console.log(i), 100);}// 方案2:IIFEfor (var i = 1; i <= 3; i++) {(function(j) {setTimeout(() => console.log(j), 100);})(i);}
四、面试真题解析
真题1:闭包与变量提升
var name = "Global";function showName() {console.log(name); // 输出什么?var name = "Local";}showName();
- 答案:
undefined(变量提升导致name在console.log时已声明但未赋值)。
真题2:作用域链的深度
var a = 1;function f1() {var a = 2;function f2() {var a = 3;console.log(a); // 输出: 3function f3() {console.log(a); // 输出: 3(沿作用域链向上查找最近定义)}f3();}f2();}f1();
真题3:闭包的内存泄漏
function heavyTask() {const largeData = new Array(1000000).fill('*');return function() {console.log('Task executed');};}const task = heavyTask(); // largeData未被释放// 修复:task = null; 解除引用
五、总结与最佳实践
- 作用域选择:优先使用
let/const避免变量污染,合理利用块级作用域。 - 闭包使用:明确闭包的生命周期,及时释放无用引用。
- 性能优化:避免在循环中创建闭包,减少作用域链的查找深度。
- 调试技巧:利用开发者工具的“Scope”面板查看闭包变量。
通过深入理解作用域、作用域链和闭包,开发者能够编写出更高效、可维护的代码,并在面试中展现扎实的JavaScript基础。