閉包是JavaScript最重要的特性之一,也是全棧/前端/JS面試的考點。
那閉包究竟該如何理解呢?
如果不愛看文字,喜歡看視頻。那本文配套講解視頻已發送到B站上供大家參考學習。
如果覺得有所收獲,可以給點個贊支持一下!
地址在這:
javascript閉包講解視頻
閉包函數的判斷和作用
閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。
那如何判斷函數是一個閉包呢?接下來我會配合一些具體的例子來對閉包問題做講解。
首先問下大家,這個G函數是否是一個閉包呢?
const F = function A(){
return function B(){
return function C(){
return function D(){
var a = 1;
return a++
}
}
}
}
const G = F()()();
for(var i=0;i<10;i++){
console.log(G())
}
一看就是不是對吧,在這里面的G函數一看就是D函數,只不過長得比較怪而已。
如果是閉包函數那應該長成這樣
const F = function A(){
var a = 1;
return function B(){
return function C(){
return function D(){
return a++
}
}
}
}
const G = F()()();
for(var i=0;i<10;i++){
console.log(G())
}
運行效果如下:
主要區別是這個變量a的聲明位置。如果a是在A中聲明的,那G就構成了閉包。也就是在G的作用域內,會形成一個名為closure作用域的子域。
那接下來第二個問題來了,這個a存在內存中的哪個位置呢?
在MDN中對JavaScript的定義是這樣的
一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函數被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函數中訪問到其外層函數的作用域。在 JavaScript 中,每當創建一個函數,閉包就會在函數創建的同時被創建出來。
好家伙,看起來就很迷。
當定義形式難以理解的時候,我們需要語義,這也說明了一件事,我們需要調試器!
進入調試器后,一切就都明朗了起來。
我們清楚地看到,當腳本運行到 D的內部時,這個Scope也就是作用域里面包含了,Local作用域,Closure作用域和Script以及Global作用域。
Local不用說了,肯定就是函數外的對象,在這里應該是window對象。
那Closure自然就是閉包作用域了。
我們依次運行時,可以清晰地看到,closure作用域內的a在不斷增加。
那第三個問題來了。
const F = function A(){
var a = 1;
return function B(){
return function C(){
return function D(){
var a = 2;
return a++
}
}
}
}
const G = F()()();
for(var i=0;i<10;i++){
console.log(G())
}
這里的G是閉包函數嗎?
答案肯定不是,因為G已經能在D中找到 a變量了,那就不需要A再提供給他了,因此我們在調試器中也看不到Closure了。
我們在這里可以看到,根本沒有了之前的Closure了。
現在第四個問題來了,這個程序的運行結果是什么?
const F = function A(){
var a = 1;
return function B(){
return function C(){
var a = 2;
return function D(){
return a++
}
}
}
}
const G = F()()();
for(var i=0;i<10;i++){
console.log(G())
}
這個是從2開始打印的,而非從1開始打印。
看到這,大家應該對閉包的優先級有認識,閉包也是離得越近優先級越高。
現在第五個問題來了,這個程序中,G的scope作用域里存在幾個閉包?
const F = function A(){
var b = 1;
return function B(){
var c = 3;
return function C(){
var a = 2;
return function D(){
b,c
return a++
}
}
}
}
const G = F()()();
for(var i=0;i<10;i++){
console.log(G())
}
答案是3個,為什么?這里有兩個角度可以解釋
- bca在D中都沒有定義,之鞥能從A,B,C中找到abc,所以這里存在三個閉包。
- 直接看調試器就知道啦
在調試器中我們能清楚地看到,這里有三個閉包。不解釋!
閉包函數的示例
1.計數功能
在閉包函數的應用中,有很多,這里舉個最常見的計數器的例子。
<html>
<head></head>
<body>
<script>
var A = (function B(){
return function C(){
var b = 0;
return function D(){
debugger
return ++b;
}
}
})()
var E = A();
var F = A();
</script>
<button onclick="console.log('E='+ E())">E++</button>
<button onclick="console.warn('F='+ F())">F++</button>
</body>
</html>
打開后運行效果如下:
點擊E++和F++后的效果
在上面的例子中我們發現,我可以用一個類似面向對象的方法,去實現計數功能。
2.setTimeout
原生的setTimeout傳遞的第一個函數不能帶參數,通過閉包可以實現傳參效果。
function func1(a) {
function func2() {
console.log(a);
}
return func2;
}
var fun = func(1);
setTimeout(fun,1000);//一秒之后打印出1
3.回調
定義行為,然后把它關聯到某個用戶事件上(點擊或者按鍵)。代碼通常會作為一個回調(事件觸發時調用的函數)綁定到事件。
比如下面這段代碼:當點擊數字時,字體也會變成相應的大小。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>測試</title>
</head>
<body>
<a href="#" id="size-12">12</a>
<a href="#" id="size-20">20</a>
<a href="#" id="size-30">30</a>
<script type="text/javascript">
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size12 = changeSize(12);
var size14 = changeSize(20);
var size16 = changeSize(30);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-20').onclick = size14;
document.getElementById('size-30').onclick = size16;
</script>
</body>
</html>
4.函數防抖
在事件被觸發n秒后再執行回調,如果在這n秒內又被觸發,則重新計時。
實現的關鍵就在於setTimeOut這個函數,由於還需要一個變量來保存計時,考慮維護全局純凈,可以借助閉包來實現。
如下代碼所示:
/*
* fn [function] 需要防抖的函數
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
let timer = null //借助閉包
return function() {
if(timer){
clearTimeout(timer) //進入該分支語句,說明當前正在一個計時過程中,並且又觸發了相同事件。所以要取消當前的計時,重新開始計時
timer = setTimeOut(fn,delay)
}else{
timer = setTimeOut(fn,delay) // 進入該分支說明當前並沒有在計時,那么就開始一個計時
}
}
}
總之閉包的用處很多,而且很廣泛。
希望這篇文章可以對大家能有所幫助!