作用域与闭包
1. 什么是作用域?
官方答案
作用域(Scope)是指程序中定义变量的区域,它决定了变量的可访问性和生命周期。JavaScript 采用词法作用域(Lexical Scope),也叫静态作用域,即作用域在代码编写时就已经确定。
作用域类型:
- 全局作用域: 在代码任何地方都能访问
- 函数作用域: 只在函数内部可访问
- 块级作用域: ES6 引入,由
{}包裹,let/const 声明的变量
通俗理解
作用域就像是权限管理系统:
- 全局作用域像公司的公共区域,所有人都能进
- 函数作用域像某个部门的办公室,只有部门内的人能进
- 块级作用域像办公室内的小隔间,更加私密
变量就像是物品,放在不同的"房间"(作用域)里,决定了谁能访问它。
详细说明
全局作用域
// 全局变量
var globalVar = 'I am global';
let globalLet = 'I am also global';
function test() {
console.log(globalVar); // 可以访问
console.log(globalLet); // 可以访问
}
// 浏览器环境中,var 声明的全局变量会挂载到 window
console.log(window.globalVar); // 'I am global'
console.log(window.globalLet); // undefined - let 不会挂载到 window
// Node.js 环境中,全局对象是 global函数作用域
function outer() {
var functionScoped = 'I am in function';
console.log(functionScoped); // 可以访问
function inner() {
console.log(functionScoped); // 可以访问外部函数的变量
}
inner();
}
outer();
console.log(functionScoped); // ReferenceError - 外部无法访问
// var 没有块级作用域
if (true) {
var a = 1;
}
console.log(a); // 1 - 可以访问!
for (var i = 0; i < 3; i++) {
// ...
}
console.log(i); // 3 - 循环后 i 依然存在!块级作用域 (ES6)
// let 和 const 有块级作用域
{
let blockScoped = 'I am in block';
const alsoBlock = 'Me too';
console.log(blockScoped); // 可以访问
}
console.log(blockScoped); // ReferenceError - 块外无法访问
// if 语句块
if (true) {
let b = 2;
}
console.log(b); // ReferenceError
// for 循环块
for (let j = 0; j < 3; j++) {
setTimeout(() => {
console.log(j); // 0, 1, 2 - 每次循环 j 都是新的变量
}, 100);
}
// 对比 var
for (var k = 0; k < 3; k++) {
setTimeout(() => {
console.log(k); // 3, 3, 3 - k 是同一个变量!
}, 100);
}作用域链
var a = 1;
function first() {
var b = 2;
function second() {
var c = 3;
function third() {
var d = 4;
// 作用域链: third -> second -> first -> global
console.log(a, b, c, d); // 1, 2, 3, 4 - 都能访问
}
third();
console.log(d); // ReferenceError - 无法访问内层变量
}
second();
}
first();
// 查找规则:
// 1. 先在当前作用域查找
// 2. 找不到就去父作用域查找
// 3. 一层层向上,直到全局作用域
// 4. 全局也没有,报 ReferenceError词法作用域 vs 动态作用域
// JavaScript 使用词法作用域(静态作用域)
// 作用域在函数定义时就确定了,而不是调用时
var value = 1;
function foo() {
console.log(value); // 词法作用域:看定义时的位置
}
function bar() {
var value = 2;
foo(); // 输出 1,不是 2
}
bar();
// 词法作用域:foo 定义时,外层是全局作用域,所以 value 是 1
// 如果是动态作用域(如 Bash):foo 调用时在 bar 内,value 是 2
// 更复杂的例子
var x = 10;
function outer() {
var x = 20;
function inner() {
console.log(x); // 20 - 定义时外层的 x
}
return inner;
}
var fn = outer();
(function() {
var x = 30;
fn(); // 输出 20,不是 30
})();变量提升与暂时性死区
// var 的变量提升
console.log(a); // undefined(不是 ReferenceError)
var a = 1;
console.log(a); // 1
// 等价于
var a; // 声明提升到顶部
console.log(a); // undefined
a = 1;
console.log(a); // 1
// let/const 的暂时性死区(TDZ)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
// 函数声明也会提升
foo(); // 可以调用
function foo() {
console.log('foo');
}
// 但函数表达式不会
bar(); // TypeError: bar is not a function
var bar = function() {
console.log('bar');
};
// TDZ 的详细示例
let x = 'outer';
function example() {
// TDZ 开始
console.log(x); // ReferenceError,不会访问外部的 x
// TDZ 结束
let x = 'inner';
}
// typeof 也不安全
console.log(typeof undeclaredVar); // 'undefined' - 未声明的变量
console.log(typeof tdzVar); // ReferenceError - TDZ 中的变量
let tdzVar;执行上下文与作用域
/*
执行上下文(Execution Context)和作用域是相关但不同的概念:
1. 作用域(Scope)
- 静态的,代码编写时就确定
- 定义变量的可访问范围
- 由代码结构决定
2. 执行上下文(Execution Context)
- 动态的,代码执行时创建
- 包含:变量环境、词法环境、this 绑定
- 形成执行上下文栈(调用栈)
3. 词法环境(Lexical Environment)
- 存储变量和函数声明
- 包含外部环境引用(实现作用域链)
*/
// 执行上下文栈的演示
function first() {
console.log('first 开始');
second();
console.log('first 结束');
}
function second() {
console.log('second 开始');
third();
console.log('second 结束');
}
function third() {
console.log('third');
}
first();
/*
执行上下文栈变化:
1. [Global]
2. [Global, first]
3. [Global, first, second]
4. [Global, first, second, third]
5. [Global, first, second]
6. [Global, first]
7. [Global]
输出:
first 开始
second 开始
third
second 结束
first 结束
*/2. 什么是闭包?
官方答案
闭包(Closure)是指有权访问另一个函数作用域中变量的函数。即使外部函数已经返回,闭包仍然可以访问外部函数的变量。
形成条件:
- 函数嵌套
- 内部函数引用了外部函数的变量
- 内部函数被返回或以某种方式被外部引用
通俗理解
闭包就像是一个背包:
当内部函数被创建时,它会把需要用到的外部变量"装进背包"带走。即使外部函数已经执行完毕,"背包"里的东西依然保留着,内部函数随时可以拿出来用。
function 外部函数() {
let 食物 = '三明治';
function 内部函数() {
console.log(食物); // 把食物装进了"背包"
}
return 内部函数;
}
const 我的背包 = 外部函数();
我的背包(); // '三明治' - 虽然外部函数已经执行完了,但食物还在!详细说明
基础闭包示例
function createCounter() {
let count = 0; // 私有变量
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count 变量被"封闭"在闭包中,外部无法直接访问
console.log(count); // ReferenceError
// 创建新的计数器,有独立的 count
const counter2 = createCounter();
console.log(counter2()); // 1 - 全新的开始闭包的实际应用
1. 数据私有化 / 模块化
const BankAccount = (function() {
// 私有变量,外部无法访问
let balance = 0;
// 公开的接口
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return `存入 ${amount} 元,余额 ${balance} 元`;
}
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return `取出 ${amount} 元,余额 ${balance} 元`;
}
return '余额不足';
},
getBalance() {
return balance;
}
};
})();
console.log(BankAccount.deposit(100)); // '存入 100 元,余额 100 元'
console.log(BankAccount.withdraw(30)); // '取出 30 元,余额 70 元'
console.log(BankAccount.getBalance()); // 70
console.log(BankAccount.balance); // undefined - 无法直接访问2. 函数柯里化
// 普通函数
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// 柯里化版本 - 使用闭包
function curry(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
const add1 = curry(1);
const add1And2 = add1(2);
console.log(add1And2(3)); // 6
// 或者链式调用
console.log(curry(1)(2)(3)); // 6
// 通用柯里化函数
function curryify(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
const sum = (a, b, c) => a + b + c;
const curriedSum = curryify(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 63. 防抖和节流
// 防抖(debounce): 一定时间内只执行最后一次
function debounce(func, wait) {
let timeout; // 闭包保存 timer
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用场景:搜索框输入
const searchInput = document.querySelector('#search');
const search = debounce(function(e) {
console.log('搜索:', e.target.value);
}, 500);
searchInput.addEventListener('input', search);
// 节流(throttle): 一定时间内只执行一次
function throttle(func, wait) {
let timeout;
let previous = 0;
return function(...args) {
const context = this;
const now = Date.now();
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
};
}
// 使用场景:滚动事件
window.addEventListener('scroll', throttle(function() {
console.log('滚动位置:', window.scrollY);
}, 200));4. 事件监听器
function createButton(label) {
let clickCount = 0; // 每个按钮独立的计数
const button = document.createElement('button');
button.textContent = label;
button.addEventListener('click', function() {
clickCount++;
console.log(`${label} 被点击了 ${clickCount} 次`);
});
return button;
}
const btn1 = createButton('按钮1');
const btn2 = createButton('按钮2');
document.body.append(btn1, btn2);
// 每个按钮有独立的 clickCount 闭包变量5. 循环中的闭包问题
// ❌ 经典错误
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3 - 都是 3!
}, 100);
}
// 原因: 所有回调共享同一个 i,循环结束时 i = 3
// ✅ 解决方案1: 使用 let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2 - 每次循环 i 是新的
}, 100);
}
// ✅ 解决方案2: IIFE 创建闭包
for (var i = 0; i < 3; i++) {
(function(j) { // 立即执行函数
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}
// ✅ 解决方案3: setTimeout 的第三个参数
for (var i = 0; i < 3; i++) {
setTimeout(function(j) {
console.log(j); // 0, 1, 2
}, 100, i);
}3. 闭包的优缺点
优点
- 数据私有化: 创建私有变量,外部无法直接访问
- 保持变量: 变量不会被垃圾回收,可以长期保持
- 模块化: 可以创建独立的模块,避免全局污染
- 函数工厂: 可以创建具有特定行为的函数
缺点
- 内存泄漏: 闭包会使变量一直保存在内存中,不当使用可能导致内存泄漏
- 性能问题: 过度使用闭包会增加内存开销
- 调试困难: 闭包中的变量在外部不可见,增加调试难度
内存管理示例
// ❌ 内存泄漏风险
function createLargeArray() {
const largeArray = new Array(1000000).fill('data'); // 占用大量内存
return function() {
console.log(largeArray.length); // 闭包引用了大数组
};
}
const fn = createLargeArray();
// largeArray 一直保存在内存中,即使只用到了它的 length
// ✅ 改进: 只保留需要的数据
function createLength() {
const largeArray = new Array(1000000).fill('data');
const length = largeArray.length; // 只保存长度
return function() {
console.log(length); // 只引用 length,数组可以被回收
};
}
const fn2 = createLength();
// ✅ 手动释放闭包
let counter = (function() {
let count = 0;
return {
increment: () => ++count,
destroy: function() {
count = null; // 释放引用
}
};
})();
// 使用完后
counter.destroy();
counter = null; // 彻底释放4. 经典面试题
题目1: 连续调用
// 实现一个函数,可以这样调用: add(1)(2)(3)
function add(a) {
// 你的代码
}
console.log(add(1)(2)(3)); // 6
console.log(add(1)(2)(3)(4)); // 10点击查看答案
function add(a) {
function sum(b) {
a = a + b; // 累加
return sum; // 返回自身,支持继续调用
}
// 重写 toString,在需要转换为原始值时调用
sum.toString = function() {
return a;
};
return sum;
}
console.log(add(1)(2)(3).toString()); // '6'
console.log(+add(1)(2)(3)); // 6 - 一元加号触发 valueOf/toString
// 或者使用 valueOf
function add2(a) {
function sum(b) {
a = a + b;
return sum;
}
sum.valueOf = function() {
return a;
};
return sum;
}考察点: 闭包、函数式编程、类型转换
题目2: 创建10个按钮
// 创建10个按钮,点击时分别弹出 0-9
for (var i = 0; i < 10; i++) {
const btn = document.createElement('button');
btn.textContent = i;
btn.onclick = function() {
alert(i); // ❌ 都会弹出 10
};
document.body.appendChild(btn);
}
// 如何修复?点击查看答案
方案1: 使用 let
for (let i = 0; i < 10; i++) { // var 改为 let
const btn = document.createElement('button');
btn.textContent = i;
btn.onclick = function() {
alert(i);
};
document.body.appendChild(btn);
}方案2: IIFE
for (var i = 0; i < 10; i++) {
(function(num) {
const btn = document.createElement('button');
btn.textContent = num;
btn.onclick = function() {
alert(num);
};
document.body.appendChild(btn);
})(i);
}方案3: 使用 dataset
for (var i = 0; i < 10; i++) {
const btn = document.createElement('button');
btn.textContent = i;
btn.dataset.index = i; // 存储到 DOM 属性
btn.onclick = function() {
alert(this.dataset.index);
};
document.body.appendChild(btn);
}方案4: bind
for (var i = 0; i < 10; i++) {
const btn = document.createElement('button');
btn.textContent = i;
btn.onclick = (function(num) {
return function() {
alert(num);
};
})(i);
document.body.appendChild(btn);
}题目3: 输出结果
function test() {
var a = 1;
return function() {
console.log(a++);
};
}
var fn = test();
fn(); // ?
fn(); // ?
var fn2 = test();
fn2(); // ?点击查看答案
答案:
1
2
1解析:
fn和fn2是两个独立的闭包- 每个闭包都有自己独立的
a变量 fn()第一次输出 1,然后 a 变成 2fn()第二次输出 2,然后 a 变成 3fn2()是新的闭包,a 从 1 开始
5. 常见面试题
问题1: 闭包是什么?有什么用?
一句话答案
闭包是函数和其词法作用域的组合,可以让函数访问其定义时所在作用域的变量,主要用于数据私有化、函数柯里化、防抖节流等场景。
详细解答
闭包的定义:
- 闭包是指一个函数能够访问其外部函数作用域中的变量,即使外部函数已经执行完毕
- 从技术角度讲,闭包 = 函数 + 函数能够访问的自由变量
形成闭包的条件:
- 函数嵌套(内部函数和外部函数)
- 内部函数引用了外部函数的变量
- 内部函数被外部引用或返回
闭包的作用:
数据私有化/封装:创建私有变量,防止全局污染
javascriptfunction createPerson(name) { let age = 0; // 私有变量 return { getName: () => name, getAge: () => age, growUp: () => ++age }; }保持变量状态:让变量持久保存在内存中
javascriptfunction createCounter() { let count = 0; return () => ++count; }函数柯里化:将多参数函数转换为单参数函数序列
javascriptfunction curry(a) { return function(b) { return a + b; }; }防抖节流:通过闭包保存timer和状态
javascriptfunction debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; }模块化开发:创建独立的模块,避免变量冲突
javascriptconst module = (function() { let private = 'private'; return { getPrivate: () => private }; })();
面试回答模板
闭包简单说就是函数能访问它定义时所在作用域的变量,即使这个函数在其他地方执行也能访问。形成闭包需要两个条件:一是函数嵌套,二是内部函数引用外部函数的变量。
闭包的作用主要有这么几个:一是做数据私有化,比如创建一个计数器,外部不能直接修改count变量,只能通过提供的方法去操作;二是保持变量状态,像防抖节流里就用闭包保存timer;三是函数柯里化,把多参数函数变成单参数的;还有就是模块化开发,避免全局污染。
需要注意的是闭包会让变量一直在内存中,用不好可能造成内存泄漏。比如在循环里创建闭包,或者闭包引用了大对象但只用了其中一小部分,这些都要小心处理。
问题2: 闭包会造成内存泄漏吗?
一句话答案
闭包本身不会造成内存泄漏,但不当使用闭包(如过度使用、引用大对象、忘记清理)会导致内存无法被回收,从而造成内存泄漏。
详细解答
内存泄漏的定义: 内存泄漏是指程序中已分配的内存无法被回收,导致可用内存不断减少。
闭包与内存的关系:
闭包的内存特性:
- 闭包会保持对外部变量的引用
- 只要闭包存在,被引用的变量就不会被垃圾回收
- 这是闭包的特性,不是bug
正常使用不会泄漏:
javascriptfunction createCounter() { let count = 0; return () => ++count; } let counter = createCounter(); // count 被保持在内存中,这是预期行为,不是泄漏 counter = null; // 解除引用后,count 可以被回收可能导致内存泄漏的场景:
场景1:闭包引用了大对象,但只用其中一小部分
javascript// ❌ 内存浪费 function getData() { const largeData = new Array(1000000).fill('data'); return function() { return largeData.length; // 只用length,但整个数组都保留了 }; } // ✅ 优化:只保留需要的 function getData() { const largeData = new Array(1000000).fill('data'); const length = largeData.length; return function() { return length; // 数组可以被回收 }; }场景2:循环中创建大量闭包
javascript// ❌ 可能的问题 for (let i = 0; i < 10000; i++) { const element = document.getElementById(`item-${i}`); const largeData = fetchData(i); // 假设返回大量数据 element.addEventListener('click', function() { console.log(largeData); // 每个闭包都保存了 largeData }); } // ✅ 优化:只保存必要信息 for (let i = 0; i < 10000; i++) { const element = document.getElementById(`item-${i}`); const id = i; // 只保存ID element.addEventListener('click', function() { const data = fetchData(id); // 需要时再获取 console.log(data); }); }场景3:DOM 引用 + 闭包导致的循环引用
javascript// ❌ 循环引用 function bindEvent() { const element = document.getElementById('btn'); const data = { /* 大量数据 */ }; element.onclick = function() { console.log(data); // 闭包引用 data // element 也被引用,即使从 DOM 移除也无法回收 }; } // ✅ 手动清理 function bindEvent() { const element = document.getElementById('btn'); const data = { /* 大量数据 */ }; element.onclick = function() { console.log(data); }; // 清理函数 return function cleanup() { element.onclick = null; // 此时闭包和 DOM 都能被回收 }; } const cleanup = bindEvent(); // 不需要时调用 cleanup();场景4:定时器未清理
javascript// ❌ 定时器持续引用 function startTimer() { const data = new Array(1000000); setInterval(function() { console.log(data.length); }, 1000); // data 永远不会被释放 } // ✅ 保存定时器ID,提供清理方法 function startTimer() { const data = new Array(1000000); const timerId = setInterval(function() { console.log(data.length); }, 1000); return function stop() { clearInterval(timerId); // 清除后 data 可以被回收 }; }
如何避免内存泄漏:
- 及时清理不需要的闭包引用
- 避免在闭包中引用大对象的全部内容
- 使用完后将闭包赋值为 null
- 清理定时器、事件监听器等
- 使用 WeakMap、WeakSet 存储对象引用
面试回答模板
闭包本身不会造成内存泄漏,但确实需要注意内存管理。闭包的特点是会保持对外部变量的引用,只要闭包还在,这些变量就不会被垃圾回收,这是它的正常特性。
但在一些场景下可能会有问题。比如闭包引用了一个很大的对象,但实际只用了其中一小部分,这时候整个对象都会被保留在内存里,就造成浪费了。还有就是在循环里创建大量闭包,每个都保存了大量数据。或者DOM元素被移除了,但事件监听器里的闭包还引用着它,导致无法回收。
避免这些问题的话,一是只在闭包里保留真正需要的数据;二是用完后及时清理,比如把闭包赋值为null,或者清除定时器和事件监听;三是可以用WeakMap这种弱引用的数据结构。总之就是要有意识地管理闭包的生命周期。
问题3: 闭包的应用场景有哪些?
一句话答案
闭包的应用场景主要包括:数据私有化/模块化、防抖节流、函数柯里化、单例模式、缓存优化、偏函数等。
详细解答
1. 数据私有化和模块化
最经典的应用,创建私有变量,只暴露公共接口:
// 单例模式
const UserManager = (function() {
let instance;
let users = []; // 私有数据
function init() {
return {
addUser(user) {
users.push(user);
},
getUsers() {
return [...users]; // 返回副本,保护原数据
},
getUserCount() {
return users.length;
}
};
}
return {
getInstance() {
if (!instance) {
instance = init();
}
return instance;
}
};
})();
const manager = UserManager.getInstance();
manager.addUser({ name: 'Alice' });2. 防抖和节流
通过闭包保存定时器ID和上次执行时间:
// 防抖:最后一次触发后才执行
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 节流:固定时间间隔执行
function throttle(fn, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
// 实际应用
const searchInput = document.querySelector('#search');
searchInput.addEventListener('input', debounce(function(e) {
console.log('搜索:', e.target.value);
}, 300));
window.addEventListener('scroll', throttle(function() {
console.log('滚动位置:', window.scrollY);
}, 200));3. 函数柯里化
将多参数函数转换为一系列单参数函数:
// 手动柯里化
function curry(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curry(1)(2)(3); // 6
// 通用柯里化函数
function currying(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
}
};
}
// 使用示例
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = currying(sum);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 64. 缓存/记忆化
利用闭包缓存计算结果,避免重复计算:
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log('从缓存获取');
return cache[key];
}
console.log('计算结果');
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// 计算斐波那契数列
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(10); // 计算
fibonacci(10); // 从缓存获取5. 偏函数
固定函数的部分参数:
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn.apply(this, [...fixedArgs, ...remainingArgs]);
};
}
// 使用示例
function multiply(a, b, c) {
return a * b * c;
}
const double = partial(multiply, 2); // 固定第一个参数为 2
console.log(double(3, 4)); // 2 * 3 * 4 = 24
const tripleOfTen = partial(multiply, 3, 10); // 固定前两个参数
console.log(tripleOfTen(5)); // 3 * 10 * 5 = 1506. 延迟执行/惰性函数
// 延迟执行
function lazy(fn) {
let result;
let executed = false;
return function(...args) {
if (!executed) {
result = fn.apply(this, args);
executed = true;
}
return result;
};
}
const expensiveOperation = lazy(function() {
console.log('执行复杂计算...');
return 42;
});
expensiveOperation(); // 执行复杂计算...
expensiveOperation(); // 直接返回结果,不再执行7. 事件处理器工厂
function createButtonHandler(buttonId, config) {
let clickCount = 0;
const maxClicks = config.maxClicks || Infinity;
return function(event) {
if (clickCount < maxClicks) {
clickCount++;
console.log(`${buttonId} 被点击第 ${clickCount} 次`);
if (config.onClick) {
config.onClick(event, clickCount);
}
if (clickCount >= maxClicks) {
console.log(`${buttonId} 已达到最大点击次数`);
this.disabled = true;
}
}
};
}
// 使用
const btn = document.querySelector('#submit');
btn.addEventListener('click', createButtonHandler('提交按钮', {
maxClicks: 1,
onClick: (e, count) => {
console.log('处理提交...');
}
}));8. 迭代器和生成器
function createIterator(array) {
let index = 0;
return {
next() {
if (index < array.length) {
return { value: array[index++], done: false };
}
return { done: true };
},
reset() {
index = 0;
}
};
}
const iterator = createIterator([1, 2, 3]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
iterator.reset();
console.log(iterator.next()); // { value: 1, done: false }面试回答模板
闭包的应用场景其实挺多的,我平时开发中经常用到。
最常见的就是数据私有化,比如做一个用户管理模块,不想让外部直接访问用户数组,就用闭包把数据保护起来,只暴露addUser、getUsers这些方法。还有单例模式也是这样实现的。
然后就是防抖节流,这个在处理输入框搜索、滚动事件时特别有用。通过闭包保存timer或者上次执行时间,来控制函数执行频率。
函数柯里化也会用到,就是把多参数函数变成单参数的,方便函数复用。比如有个三个参数的add函数,柯里化后可以先固定一个参数,返回新函数继续接收剩余参数。
还有缓存优化,像计算斐波那契数列这种递归函数,用闭包把计算结果缓存起来,避免重复计算,性能提升很明显。
实际项目里还会用闭包做偏函数、延迟执行、事件处理器工厂这些。总之闭包的核心价值就是能保持状态和实现数据封装,很多设计模式都离不开它。
总结
核心要点
- 作用域: 变量的可访问范围,JS 使用词法作用域
- 作用域链: 从内到外逐层查找变量
- 闭包: 函数 + 其词法环境的组合
- 闭包应用: 数据私有、柯里化、防抖节流、模块化
- 注意内存: 避免不必要的闭包,及时释放引用
面试加分项
- 能用代码演示闭包的实际应用
- 了解闭包的内存模型
- 知道如何避免内存泄漏
- 理解 V8 中的上下文和作用域链
- 能手写防抖、节流、柯里化等工具函数
6. 高级面试题
题目4:分析代码输出
var a = 10;
function foo() {
console.log(a);
}
(function() {
var a = 20;
foo(); // ?
})();点击查看答案
答案: 10
解析: JavaScript 使用词法作用域(静态作用域)。foo 函数定义在全局作用域,所以它访问的 a 是全局的 a(值为 10),而不是 IIFE 内部的 a(值为 20)。
作用域在函数定义时确定,而不是调用时确定。
题目5:实现私有变量
// 使用闭包实现一个类,具有私有变量 name 和 age
// 提供 getName、getAge、setAge 方法
// age 只能设置为 0-150 之间的整数点击查看答案
function Person(name, age) {
// 私有变量
let _name = name;
let _age = validateAge(age) ? age : 0;
function validateAge(val) {
return Number.isInteger(val) && val >= 0 && val <= 150;
}
// 公开方法
this.getName = function() {
return _name;
};
this.getAge = function() {
return _age;
};
this.setAge = function(newAge) {
if (validateAge(newAge)) {
_age = newAge;
return true;
}
return false;
};
}
// 测试
const person = new Person('Alice', 25);
console.log(person.getName()); // 'Alice'
console.log(person.getAge()); // 25
console.log(person._name); // undefined(无法直接访问)
console.log(person._age); // undefined
person.setAge(30);
console.log(person.getAge()); // 30
person.setAge(-1);
console.log(person.getAge()); // 30(无效设置被拒绝)
// ES6 WeakMap 实现私有变量
const privateData = new WeakMap();
class PersonES6 {
constructor(name, age) {
privateData.set(this, { name, age });
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
}题目6:实现 once 函数
// 实现一个 once 函数,让传入的函数只执行一次
function once(fn) {
// 你的代码
}
const logOnce = once(console.log);
logOnce('a'); // 'a'
logOnce('b'); // 无输出
logOnce('c'); // 无输出点击查看答案
// 基础版本
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
// 增强版本:支持重置
function onceWithReset(fn) {
let called = false;
let result;
const onceFn = function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
onceFn.reset = function() {
called = false;
result = undefined;
};
return onceFn;
}
// 测试
const init = once(function() {
console.log('初始化');
return { initialized: true };
});
console.log(init()); // '初始化' { initialized: true }
console.log(init()); // { initialized: true }(不再执行,但返回之前的结果)题目7:实现函数组合 compose
// 实现 compose 函数,从右到左执行函数
// compose(f, g, h)(x) === f(g(h(x)))点击查看答案
// 方式1:reduce
function compose(...fns) {
if (fns.length === 0) return arg => arg;
if (fns.length === 1) return fns[0];
return fns.reduce((a, b) => (...args) => a(b(...args)));
}
// 方式2:reduceRight
function compose2(...fns) {
return function(initialValue) {
return fns.reduceRight((acc, fn) => fn(acc), initialValue);
};
}
// 方式3:递归
function compose3(...fns) {
return function(value) {
if (fns.length === 0) return value;
const last = fns.pop();
return compose3(...fns)(last(value));
};
}
// 测试
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;
const composed = compose(add10, multiply2, subtract5);
console.log(composed(10)); // ((10 - 5) * 2) + 10 = 20
// pipe:从左到右执行(compose 的反向)
function pipe(...fns) {
return fns.reduce((a, b) => (...args) => b(a(...args)));
}
const piped = pipe(subtract5, multiply2, add10);
console.log(piped(10)); // 20题目8:分析复杂闭包
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result[i] = function(num) {
return function() {
return num;
};
}(i);
}
return result;
}
var funcs = createFunctions();
console.log(funcs[0]()); // ?
console.log(funcs[1]()); // ?
console.log(funcs[2]()); // ?点击查看答案
答案:
0
1
2解析: 这里使用了 IIFE(立即调用函数表达式)来创建闭包。每次循环时,i 的值作为参数 num 传入外层函数并立即执行,返回的内部函数保存了那个时刻的 num 值。
关键点:
function(num) { return function() { return num; }; }(i)- 外层函数立即执行,
i的值被保存到num参数中 - 返回的内部函数形成闭包,保存了
num的值
如果没有 IIFE:
for (var i = 0; i < 3; i++) {
result[i] = function() {
return i; // 共享同一个 i
};
}
// 全部输出 3题目9:实现 bind 函数
// 手写实现 Function.prototype.bind点击查看答案
Function.prototype.myBind = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Bind must be called on a function');
}
const self = this;
const fBound = function(...innerArgs) {
// 处理 new 调用的情况
// 如果是 new 调用,this 指向新创建的对象
return self.apply(
this instanceof fBound ? this : context,
[...args, ...innerArgs]
);
};
// 维护原型链
if (this.prototype) {
fBound.prototype = Object.create(this.prototype);
}
return fBound;
};
// 测试
function greet(greeting, punctuation) {
return `${greeting}, ${this.name}${punctuation}`;
}
const person = { name: 'Alice' };
const boundGreet = greet.myBind(person, 'Hello');
console.log(boundGreet('!')); // 'Hello, Alice!'
// 测试 new 调用
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
const BoundAnimal = Animal.myBind(null, 'Dog');
const dog = new BoundAnimal();
console.log(dog.name); // 'Dog'
console.log(dog.speak()); // 'Dog makes a sound'
console.log(dog instanceof Animal); // true题目10:实现 sleep 函数
// 实现一个 sleep 函数,使得 await sleep(1000) 后暂停 1 秒点击查看答案
// 基础版本
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用
async function demo() {
console.log('开始');
await sleep(1000);
console.log('1秒后');
}
// 带返回值的版本
function sleepWithValue(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
async function demo2() {
const result = await sleepWithValue(1000, 'done');
console.log(result); // 'done'
}
// 可取消的版本
function cancellableSleep(ms) {
let timeoutId;
let rejectFn;
const promise = new Promise((resolve, reject) => {
rejectFn = reject;
timeoutId = setTimeout(resolve, ms);
});
promise.cancel = function() {
clearTimeout(timeoutId);
rejectFn(new Error('Sleep cancelled'));
};
return promise;
}
async function demo3() {
const sleepPromise = cancellableSleep(5000);
setTimeout(() => {
sleepPromise.cancel();
}, 1000);
try {
await sleepPromise;
console.log('完成');
} catch (e) {
console.log(e.message); // 'Sleep cancelled'
}
}7. V8 引擎中的闭包
闭包的内存模型
/*
V8 引擎如何处理闭包:
1. 作用域分析(Scope Analysis)
- 编译阶段分析哪些变量被内部函数引用
- 被引用的变量会被标记为"上下文变量"
2. 上下文对象(Context Object)
- 闭包变量存储在堆上的 Context 对象中
- 不是存储在栈上(栈上的会被销毁)
3. 内存布局
- 普通局部变量:栈上分配
- 闭包变量:堆上的 Context 对象
4. 垃圾回收
- 当闭包函数不再被引用时
- Context 对象才能被垃圾回收
*/
function createClosure() {
let a = 1; // 被内部函数引用 → 堆上的 Context
let b = [1,2,3]; // 被内部函数引用 → 堆上的 Context
let c = 'temp'; // 未被引用 → 栈上,函数返回后销毁
return function() {
console.log(a, b); // a 和 b 被引用
};
}
const closure = createClosure();
// c 已被销毁
// a 和 b 保存在 Context 对象中Chrome DevTools 查看闭包
function outer() {
const name = 'closure variable';
return function inner() {
console.log(name);
};
}
const fn = outer();
// 在 Chrome DevTools 中:
// 1. 打断点在 inner 函数内
// 2. 查看 Scope 面板
// 3. 可以看到 Closure 作用域
// 4. 里面包含 name 变量
// 或者直接打印函数
console.dir(fn);
// [[Scopes]] 属性中可以看到闭包信息闭包优化建议
// 1. 避免在闭包中引用不需要的大对象
// ❌ 不好
function createHandler() {
const largeData = new Array(1000000).fill('x');
return function handler() {
// 只用了 length,但整个数组都被保留
return largeData.length;
};
}
// ✅ 好
function createHandler() {
const largeData = new Array(1000000).fill('x');
const length = largeData.length; // 只保存需要的
return function handler() {
return length;
};
}
// 2. 及时解除闭包引用
let cached = (function() {
const data = computeExpensiveData();
return {
getData: () => data,
clear: function() {
// 提供清理方法
}
};
})();
// 不再需要时
cached = null;
// 3. 考虑使用 WeakMap 存储私有数据
const privateStore = new WeakMap();
class MyClass {
constructor(secret) {
privateStore.set(this, { secret });
}
getSecret() {
return privateStore.get(this).secret;
}
}
// 实例被回收时,privateStore 中对应的数据也会被回收
// 4. 使用 WeakRef 和 FinalizationRegistry(ES2021)
const registry = new FinalizationRegistry((heldValue) => {
console.log(`${heldValue} 被垃圾回收了`);
});
function createCached(key, data) {
const obj = { data };
registry.register(obj, key);
return new WeakRef(obj);
}
const weakRef = createCached('myData', { value: 42 });
// 稍后检查对象是否还存在
const obj = weakRef.deref();
if (obj) {
console.log(obj.data);
} else {
console.log('对象已被回收');
}8. 实战应用
React 中的闭包陷阱
// React Hooks 中常见的闭包问题
function Counter() {
const [count, setCount] = useState(0);
// ❌ 闭包陷阱:count 是旧值
useEffect(() => {
const timer = setInterval(() => {
console.log('当前 count:', count); // 始终是 0
setCount(count + 1); // 始终设置为 1
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组,只在挂载时执行
// ✅ 解决方案1:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ 解决方案2:使用 ref
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const timer = setInterval(() => {
console.log('当前 count:', countRef.current); // 正确的值
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
// ✅ 解决方案3:正确设置依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // count 变化时重新创建定时器
return <div>{count}</div>;
}事件处理器的闭包
// ❌ 内存泄漏风险
function setupListeners() {
const heavyData = loadHeavyData();
document.getElementById('btn').addEventListener('click', function() {
// heavyData 被闭包捕获,永远不会释放
console.log(heavyData.length);
});
}
// ✅ 提供清理机制
function setupListeners() {
const heavyData = loadHeavyData();
const handler = function() {
console.log(heavyData.length);
};
document.getElementById('btn').addEventListener('click', handler);
// 返回清理函数
return function cleanup() {
document.getElementById('btn').removeEventListener('click', handler);
};
}
const cleanup = setupListeners();
// 不需要时
cleanup();
// ✅ 使用 AbortController(现代方式)
function setupListeners() {
const controller = new AbortController();
const heavyData = loadHeavyData();
document.getElementById('btn').addEventListener('click', function() {
console.log(heavyData.length);
}, { signal: controller.signal });
return () => controller.abort();
}请求缓存
// 使用闭包实现请求缓存
function createRequestCache() {
const cache = new Map();
const pending = new Map();
return async function cachedRequest(url, options = {}) {
const cacheKey = `${url}_${JSON.stringify(options)}`;
// 检查缓存
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
if (Date.now() - cached.timestamp < (options.maxAge || 60000)) {
return cached.data;
}
cache.delete(cacheKey);
}
// 检查是否有正在进行的相同请求
if (pending.has(cacheKey)) {
return pending.get(cacheKey);
}
// 发起请求
const requestPromise = fetch(url, options)
.then(res => res.json())
.then(data => {
cache.set(cacheKey, { data, timestamp: Date.now() });
pending.delete(cacheKey);
return data;
})
.catch(err => {
pending.delete(cacheKey);
throw err;
});
pending.set(cacheKey, requestPromise);
return requestPromise;
};
}
const cachedFetch = createRequestCache();
// 使用
async function loadData() {
// 相同 URL 的请求会被缓存
const data1 = await cachedFetch('/api/users');
const data2 = await cachedFetch('/api/users'); // 从缓存获取
}