一文讀懂閉包函數

js閉包:
根據《javascript權威指南 第6版》所說
在javascript語言中,閉包就是函數和該函數作用域的組合。從這個概念上來講,在js中,所有函數都是閉包

既然所有函數都是閉包,還有必要專門提這個概念嗎?

大多數函數被調用時(invoked),使用的作用域和他們被定義時(defined)使用的作用域是同一個作用域,這種情況下,閉包神馬的,無關緊要。但是,當他們被invoked的時候,使用的作用域不同於他們定義時使用的作用域的時候,閉包就會變的非常有趣,並且開始有了很多的使用場景,這就是你之所以要掌握閉包的原因。 理解“閉包” step 1:掌握嵌套函數的詞法作用域規則(lexical scoping rules)

var scope = "global scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkScope();   //=> "local scope"

分析一下上面的代碼,該代碼定義了一個全局變量 scope,以及一個函數checkScope,在函數checkScope中,定義一個局部變量,同樣命名爲scope,以及一個函數f(嵌套函數)

注意:
在js中,函數可以用來創建函數作用域;
函數就像一層半透明玻璃,在函數內部可以看到函數外部的變量,但是在函數外部,看不到函數內部的變量。
變量的搜索是從內向外而不是從外向內搜索的。

代碼執行過程分析:在這裏插入圖片描述
checkScope被invoke時,return f(),運行內部嵌套函數f,f沿着作用域鏈從內向外尋找變量scope,找到“local scope”,停止尋找,因此,函數返回 “local scope”;

接下來,代碼稍作修改:

var scope = "global scope"; 
function checkScope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f;
}
checkScope()();   //=> "這次返回什麼?"

代碼執行過程分析:
checkScope被invoke時,將內部嵌套的函數f返回,因此checkScope()()這句執行時,其實運行的是f(),f函數返回scope變量,在這種情況下,f會從哪個作用域裏去尋找變量scope呢?

remember 詞法作用域的基礎規則:函數被執行時(executed)使用的作用域鏈(scope chain)是被定義時的scope chain,而不是執行時的scope chain

嵌套函數f(), 被定義時,所在的作用域鏈中,變量scope是被綁定的值是“local scope”,而不是"global scope",因此,以上代碼的結果是啥?沒錯,是"local scope"!

這就是閉包的神奇特性:閉包可以捕獲到局部變量和參數的外部函數綁定,即便外部函數的調用已經結束。
只要記住一點:詞法作用域的規則,即函數被執行時(executed)使用的作用域鏈(scope chain)是被定義時的scope chain,而不是執行時的scope chain,就可以很容易的理解閉包的行爲了。

理解“閉包” step 2:掌握閉包的使用場景
在js版本的設計模式中,很多模式的實現都需要藉助於閉包,因此,掌握閉包的使用場景,可以結合設計模式一起理解學習。這裏引用了《JavaScript設計模式與開發實踐》中的很多例子,書很好,推薦大家閱讀學習。

閉包經典使用場景一:通過循環給頁面上多個dom節點綁定事件
場景描述:假如頁面上有5個button,要給button綁定onclick事件,點擊的時候,彈出對應button的索引編號。
在這裏插入圖片描述

<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
</head>
<body>
    <button>Button0</button>
    <button>Button1</button>
    <button>Button2</button>
    <button>Button3</button>
    <button>Button4</button>
</body>
</html>

先隨手來一段for循環:

var btns = document.getElementsByTagName(‘button’);
for(var i = 0, len = btns.length; i < len; i++) {
btns[i].onclick = function() {
alert(i);
}
}

通過執行該段代碼,發現不論點擊哪個button ,均alert 5;

why?
因爲,onclick事件是被異步觸發的,當事件被觸發時,for循環早已結束,此時變量 i 的值已經是 5 。所以,當onlick事件函數順着作用域鏈從內向外查找變量 i 時,找到的值總是 5 。

那怎麼能循環給button添加事件,並且還能alert出來不同的值呢?答案當然是:“閉包”!在閉包的作用下,定義事件函數的時候,每次循環的i值都被封閉起來,這樣在函數執行時,會查找定義時的作用域鏈,這個作用域鏈裏的i值是在每次循環中都被保留的,因此點擊不同的button會alert出來不同的i。

上代碼:
Tip: 在js中,沒有塊級作用域 ,只有函數作用域。可以採用“立即執行函數Immediately-Invoked Function Expression (IIFE)”的方式創建作用域。

for(var i = 0, len = btns.length; i < len; i++) {
    (function(i) {
        btns[i].onclick = function() {
            alert(i);
        }
    }(i))
}

運行以上代碼,是符合我們需求的。

閉包使用場景二:封裝變量
閉包可以將一些不希望暴露在全局的變量封裝成“私有變量”。
假如有一個計算乘積的函數,mult函數接收一些number類型的參數,並返回乘積結果。爲了提高函數性能,我們增加緩存機制,將之前計算過的結果緩存起來,下次遇到同樣的參數,就可以直接返回結果,而不需要參與運算。這裏,存放緩存結果的變量不需要暴露給外界,並且需要在函數運行結束後,仍然保存,所以可以採用閉包。
上代碼:

var mult = (function(){
    var cache = {};
    var calculate = function() {
        var a = 1;
        for(var i = 0, len = arguments.length; i < len; i++) {
            a = a * arguments[i];
        }
        return a;
    }
    
    return function() {
        var args = Array.prototype.join.call(arguments, ',');
        if(args in cache) {
            return cache[args];
        }
        
        return cache[args] = calculate.apply(null, arguments);
    }
}())

閉包使用場景三:延續局部變量的壽命
img對象經常用於數據上報,如下:

var report = function(src) {
    var img = new Image();
    img.src = src;
}
report('http://xxx.com/getUserInfo');

這段代碼在運行時,發現在一些低版本瀏覽器上存在bug,會丟失部分數據上報,原因是img是report函數中的局部變量,當report函數調用結束後,img對象隨即被銷燬,而此時可能還沒來得及發出http請求,所以此次請求就會丟失。
因此,我們使用閉包把img對象封閉起來,就可以解決數據丟失的問題:
var report = (function() {
var imgs = [];
return function(src) {
var img = new Image();
imgs.push(img);
img.src = src;
}
}())

閉包+設計模式

在諸多設計模式中,閉包都有廣泛的應用。對象==數據+方法。而閉包是在過程中以環境的形式包含了數據。因此,通常面向對象能實現的功能,使用閉包也可以實現。
涉及到設計模式,閉包就是一種理所當然的存在,必須熟練使用,纔可以理解每種設計模式的意圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章