閉包是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) // 進入該分支說明當前並沒有在計時,那麼就開始一個計時
}
}
}
總之閉包的用處很多,而且很廣泛。
希望這篇文章可以對大家能有所幫助!