很多前端都感觉自己没用过闭包,当面试官问到:
“现实开发中哪里会用到闭包”
直接哑语。
我们真的没用到闭包嘛?
闭包(Closure) 是 JavaScript 中一个非常重要的概念,它涉及到函数、作用域以及变量的生命周期。简单来说,闭包是一个函数,它能够记住并访问定义时的作用域,即使这个函数在外部作用域执行时,仍然能够访问到原本定义时的局部变量。
闭包的形成条件是:函数内部引用了外部函数的变量,并且这个函数在外部被调用时依然能够访问这些外部变量。换句话说,闭包是一个 “函数 + 词法作用域” 的组合。
function outer() {
let counter = 0;
// 内部函数
function inner() {
counter++;
console.log(counter);
}
return inner;
}
const myClosure = outer(); // 返回了一个闭包
myClosure(); // 输出: 1
myClosure(); // 输出: 2
outer
是外部函数,它定义了一个局部变量 counter
,并返回了内部函数 inner
。inner
是一个闭包,它引用了外部函数 outer
中的 counter
变量。outer
执行完毕,闭包 inner
仍然能够访问到 counter
变量,保持 counter
的状态。总之,闭包是 JavaScript 中非常强大和灵活的特性,它通过保持对外部变量的引用,使得函数的行为更加灵活和强大。
在日常开发中,闭包的应用非常广泛,下面列举一些常见的场景。
在 setTimeout
或 setInterval
中,常常使用闭包来记住当前的变量或状态,进行延迟操作。
例如:
function countdown(seconds) {
let timeLeft = seconds;
const timer = setInterval(function() {
console.log(timeLeft);
timeLeft--;
if (timeLeft <= 0) {
clearInterval(timer);
console.log("Time's up!");
}
}, 1000);
}
countdown(5);
这里,setInterval
中的回调函数是一个闭包,它访问了外部函数 countdown
中的 timeLeft
变量。
在处理大量的事件(如 scroll
、resize
或 keypress
)时,常使用闭包来实现防抖(Debounce)和节流(Throttle),从而避免频繁执行。
防抖示例:
function debounce(func, delay) {
let timeout;
return function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, arguments);
}, delay);
};
}
const log = debounce(() => console.log('Function executed!'), 1000);
window.addEventListener('resize', log);
这里,debounce
函数返回一个闭包,确保在用户停止触发事件后的延迟时间内才会执行实际的回调函数。
在使用回调函数的异步编程中,闭包可以保持对外部变量的访问,确保异步操作完成时,能正确访问这些变量。
例如,异步请求中的闭包使用:
function fetchData(url) {
let dataLoaded = false;
// 假设 fetch 请求完成后会调用回调函数
fetch(url)
.then(response => response.json())
.then(data => {
dataLoaded = true;
console.log('Data fetched:', data);
});
return function() {
if (dataLoaded) {
console.log('Data has been loaded');
} else {
console.log('Loading data...');
}
};
}
const checkDataStatus = fetchData('https://api.example.com/data');
checkDataStatus(); // 可能输出: 'Loading data...'
这里,checkDataStatus
函数是一个闭包,它能访问 fetchData
中的 dataLoaded
变量。
闭包广泛应用于 JavaScript 模块化开发,用于将变量和方法封装在函数作用域内,避免污染全局作用域。
例如:
const module = (function() {
let privateVar = 'I am private';
function privateMethod() {
console.log(privateVar);
}
return {
publicMethod: function() {
privateMethod();
}
};
})();
module.publicMethod(); // 输出: I am private
在这个例子中,privateVar
和 privateMethod
是私有的,外部只能访问公开的 publicMethod
。 在 Vue.js 中,闭包的使用并不像在 JavaScript 中直接调用函数时那么显而易见,但仍然有一些地方可以体现闭包的特性,主要体现在事件处理、计算属性、生命周期钩子和组件方法中。以下是 Vue 中一些常见的闭包应用场景:
在 Vue 中,经常会使用事件处理方法。由于 Vue 会自动处理事件绑定,这时方法本身可能会变成一个闭包,能够访问它的外部作用域中的变量。
例如,假设有一个按钮,点击时增加一个计数值:
<template>
<button @click="increment">Click me</button>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
console.log(this.count);
}
}
};
</script>
在这个例子中,increment
方法是一个闭包,它可以访问 Vue 组件的 data
中的 count
变量,即使该方法是通过 @click
事件触发的。即使 increment
方法和事件处理函数被分离,它仍然能访问到 this.count
,因为它保存了对外部作用域的引用。
计算属性是 Vue 中的一个重要特性,它可以依赖其他数据,并在依赖的数据变化时自动重新计算。计算属性背后也有闭包的实现:每当计算属性被访问时,它会闭包保存相关的响应式依赖,直到计算属性的值被更新。
<template>
<div>
<p>{{ doubledCount }}</p>
<button @click="count++">Increase Count</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
computed: {
doubledCount() {
return this.count * 2;
}
}
};
</script>
在这个例子中,doubledCount
是一个计算属性,它闭包保存了 this.count
的依赖关系。每当 this.count
发生变化时,doubledCount
会自动重新计算。
Vue 组件中的生命周期钩子,如 created
、mounted
、updated
等,通常会引用组件的数据或方法,因此它们本质上也是闭包。这意味着即使在组件的生命周期内某些状态发生变化,这些钩子函数仍能访问到组件的上下文。
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello, Vue!"
};
},
mounted() {
setTimeout(() => {
this.message = "Hello, World!"; // 这里的箭头函数是闭包
}, 1000);
}
};
</script>
这里,mounted
钩子内的箭头函数是一个闭包,它能够访问到 this.message
,即使 setTimeout
是异步执行的。
Vue 方法(特别是在 methods
中定义的那些)可能会变成闭包,并且这些方法可以访问到组件实例中的所有数据和方法。在异步操作中,闭包特别有用,因为它们能保证访问到正确的上下文(即 this
)。
<template>
<div>
<p>{{ message }}</p>
<button @click="delayedChangeMessage">Change message</button>
</div>
</template>
<script>
export default {
data() {
return {
message: "Initial message"
};
},
methods: {
delayedChangeMessage() {
setTimeout(() => {
this.message = "Updated message"; // 闭包保存了对this的引用
}, 2000);
}
}
};
</script>
这里,delayedChangeMessage
方法内的箭头函数是闭包,它能够引用到 this.message
,即使延迟了两秒钟才执行。
在 Vue 的 v-for
循环中,也可能会使用闭包,尤其是当每个循环项都绑定了事件时。循环中的每个函数都会形成闭包,保存其循环上下文。
<template>
<div>
<ul>
<li v-for="(item, index) in items" :key="index" @click="showItem(index)">
{{ item }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
items: ['Item 1', 'Item 2', 'Item 3']
};
},
methods: {
showItem(index) {
alert('Item index: ' + index);
}
}
};
</script>
在上面的代码中,showItem
方法通过 v-for
中的每个 li
元素绑定了事件监听器。每个事件处理程序都是一个闭包,它可以访问到 index
变量,即使这些事件处理函数是在不同的上下文中调用的。
还有没有其他常有的?欢迎各位大佬在评论区留言讨论