
1. 作用域变量存取的规则手册在JavaScript中作用域就像一套严格的访问控制规则决定了变量在哪里可以被访问在哪里会被隐藏。理解作用域是避免变量冲突和内存泄漏的基础。1.1 全局作用域与局部作用域全局作用域就像公共场所任何代码都可以访问这里的变量。在浏览器环境中全局作用域是window对象var globalVar 我在全局; console.log(window.globalVar); // 输出我在全局函数作用域则是私人空间只有函数内部才能访问function testScope() { var localVar 我是局部变量; console.log(localVar); // 正常输出 } console.log(localVar); // ReferenceError: localVar is not defined1.2 let/const带来的块级作用域ES6引入的let和const带来了块级作用域由{}界定这是与var最显著的区别if (true) { var varVariable var声明; let letVariable let声明; } console.log(varVariable); // 输出var声明 console.log(letVariable); // ReferenceError块级作用域在循环中特别有用能解决经典的闭包陷阱// 使用var的问题 for (var i 0; i 3; i) { setTimeout(() console.log(i), 100); // 输出3,3,3 } // 使用let的解决方案 for (let j 0; j 3; j) { setTimeout(() console.log(j), 100); // 输出0,1,2 }1.3 作用域链的查找机制当访问一个变量时JavaScript会沿着作用域链向上查找let outer 外层; function parent() { let middle 中间; function child() { let inner 内层; console.log(inner); // 内层 console.log(middle); // 中间 console.log(outer); // 外层 console.log(global); // ReferenceError } child(); } parent();查找顺序是当前作用域 → 父作用域 → ... → 全局作用域。如果到全局作用域还没找到就会抛出ReferenceError。2. 闭包跨越作用域的魔法桥梁闭包是JavaScript中最强大的特性之一也是面试中最常被深挖的话题。简单说闭包让函数可以记住并访问其词法作用域即使函数在其作用域外执行。2.1 闭包的形成条件一个典型的闭包需要三个要素外层函数包裹内层函数内层函数引用外层函数的变量内层函数被外部引用function createCounter() { let count 0; // 外层变量 return function() { return count; // 内层函数引用外层变量 }; } const counter createCounter(); // 外部引用 console.log(counter()); // 1 console.log(counter()); // 22.2 闭包的实际应用场景2.2.1 数据封装与私有变量在没有class私有字段(#)的年代闭包是实现私有变量的主要方式function createPerson(name) { let _age 0; // 私有变量 return { getName: () name, getAge: () _age, birthday: () { _age; } }; } const person createPerson(Alice); console.log(person._age); // undefined person.birthday(); console.log(person.getAge()); // 12.2.2 函数工厂与柯里化闭包可以实现函数工厂和参数预置柯里化function multiplyFactory(x) { return function(y) { return x * y; }; } const double multiplyFactory(2); const triple multiplyFactory(3); console.log(double(5)); // 10 console.log(triple(5)); // 152.2.3 事件处理与回调在事件处理中闭包可以保存状态function setupButtons() { const buttons document.querySelectorAll(button); for (let i 0; i buttons.length; i) { buttons[i].addEventListener(click, function() { console.log(按钮${i}被点击); }); } }2.3 闭包的内存管理闭包会导致外层函数的变量无法被垃圾回收可能引发内存泄漏。常见的内存泄漏场景// 意外的全局变量 function leak1() { leakedVar 我泄漏了; // 忘记声明变成全局变量 } // 定时器未清理 function leak2() { const data getHugeData(); setInterval(() { console.log(data.length); }, 1000); } // DOM引用未清除 function leak3() { const element document.getElementById(big-element); element.addEventListener(click, () { console.log(点击); }); // 即使移除DOM由于事件监听器引用element不会被回收 }解决方案使用严格模式避免意外全局变量清除不再需要的定时器、事件监听器对于大型数据使用后手动解除引用3. 立即执行函数(IIFE)创建独立作用域的利器立即执行函数表达式(Immediately Invoked Function Expression)是定义后立即执行的函数主要用于创建独立作用域。3.1 IIFE的基本语法两种常见写法// 写法1括号包裹整个函数 (function() { console.log(IIFE执行); })(); // 写法2括号包裹函数体 (function() { console.log(IIFE执行); }());3.2 IIFE的实际应用3.2.1 避免变量污染全局在模块化之前IIFE是隔离作用域的主要方式// 全局命名空间污染 var count 0; function add() { count; } // 使用IIFE隔离 (function() { var count 0; function add() { count; } window.myModule { add }; })(); console.log(count); // 0不受IIFE内部影响3.2.2 捕获瞬间状态在循环中IIFE可以捕获每次迭代的状态for (var i 0; i 3; i) { (function(currentI) { setTimeout(function() { console.log(currentI); }, 100); })(i); } // 输出0,1,23.2.3 模块化开发的基础早期模块化方案如UMD基于IIFEvar MyModule (function() { var privateVar 私有; function privateMethod() { /*...*/ } return { publicMethod: function() { privateMethod(); console.log(privateVar); } }; })();3.3 现代替代方案随着ES6模块和块级作用域的普及IIFE的使用减少了// 使用块级作用域替代 { let count 0; const add () count; window.myModule { add }; } // 使用模块化 // module.js let count 0; export function add() { count; } // main.js import { add } from ./module.js;4. 高频面试题深度解析4.1 作用域相关题目题目1以下代码输出什么var a 1; function foo() { console.log(a); var a 2; } foo();解析输出undefined。这是因为变量提升(hoisting)函数内的var a会提升到函数顶部相当于function foo() { var a; console.log(a); a 2; }题目2let和var的区别关键区别var有函数作用域let有块级作用域var会变量提升let存在暂时性死区(TDZ)var可以重复声明let不可以var在全局声明会成为window属性let不会4.2 闭包相关题目题目1实现一个计数器工厂function createCounter() { let count 0; return { increment: () count, decrement: () --count, getCount: () count }; } const counter createCounter();题目2以下代码的输出顺序是什么for (var i 0; i 3; i) { setTimeout(function() { console.log(i); }, 100); }输出3,3,3。因为setTimeout回调执行时循环已结束i的值为3。解决方案使用IIFE捕获i使用let替代var使用setTimeout的第三个参数4.3 IIFE相关题目题目1以下IIFE的输出是什么var result (function(x) { return x * 2; })(5); console.log(result); // 10题目2如何用IIFE实现模块模式var calculator (function() { let memory 0; function add(x) { memory x; } function get() { return memory; } return { add, get }; })();4.4 综合应用题题目实现一个once函数确保传入函数只执行一次function once(fn) { let called false; let result; return function(...args) { if (!called) { called true; result fn.apply(this, args); } return result; }; } const logOnce once(console.log); logOnce(hello); // 输出hello logOnce(world); // 无输出5. 实战中的经验与陷阱5.1 作用域相关的常见错误意外全局变量忘记声明变量导致污染全局function oops() { accidentalGlobal 糟糕; // 应该是let/const/var }提示始终使用严格模式(use strict)可以避免这种错误循环中的闭包陷阱使用var在循环中创建闭包for (var i 0; i 5; i) { setTimeout(() console.log(i), 100); // 输出5个5 }解决方案使用let替代var使用IIFE捕获i使用Array.prototype.forEach5.2 闭包性能优化技巧减少闭包捕获的变量数量只保留必要的变量// 不推荐捕获了整个data对象 function process() { const data getLargeData(); return function() { console.log(data.id); }; } // 推荐只捕获需要的属性 function process() { const data getLargeData(); const { id } data; return function() { console.log(id); }; }及时释放闭包引用function setup() { const data getLargeData(); const listener () console.log(data); element.addEventListener(click, listener); // 不再需要时移除 return () element.removeEventListener(click, listener); }5.3 IIFE的现代替代方案使用块级作用域对于简单的隔离需求{ let temp 临时变量; // 这里使用temp } // temp在这里不可访问使用ES模块对于代码组织// module.js let privateVar 私有; export function publicFn() { /*...*/ } // main.js import { publicFn } from ./module.js;使用类私有字段对于面向对象场景class Counter { #count 0; // 私有字段 increment() { this.#count; } }