你覺得什麼是閉包(適用於學習積累和麪試)

寫在前面

初次接觸到閉包這個概念的時候,我嘗試在互聯網上找到可靠的描述,蒐羅到的答案能夠說是五花八門,每個帖子看完以後我都感受彷彿完全明瞭,可是本身張嘴卻說不出個因此然,又一次重現瞭如下場景: 眼睛:已瀏覽。腦子:已過,穩了。嘴:啥?啥玩意兒? 無奈之下買了js原理的相關書籍,細細品味了一番,發現書裏寫的不少,也很詳盡,可是概念終究太過抽象,有時候同一個概念,換一個表現形式,就容易失去聯想。這也揭示一個猿們的通病,單純看懂一個技術點並不難,難的是運用,以及同一個點的不一樣形式的變換。javascript

別的觀點

那麼什麼叫閉包?觀點不少,出現頻率最高的有如下兩個觀點:java

  1. 函數套函數。
  2. 在函數外獲取函數內變量的技術。

單純的評價這兩個觀點,顯然都不算錯,由於閉包不論是從形式上仍是表現上確實涵蓋了以上特色,但這兩個觀點並不許確,屬於閉包的必要不充分條件。咱們來嘗試推翻這兩個觀點。編程

首先說第二點,這個觀點在沒有先決條件下,能夠說是至關的不嚴謹,一個簡單的例子:bash

function fun() {
    var innerVal = '內部變量'return innerVal;
}
var getInnerVal = fun ();
console.log(getInnerVal);
複製代碼

根據以上例子,咱們確實得到了函數fun的內部變量,可是這跟閉包有關係麼?毫無關係。閉包

從函數開始

下面咱們來講上面的第一點,從這兒開始,咱們正式認識下閉包。 閉包這項技術離不開函數,由於函數是閉包的基本組成部分,因此咱們先談談函數,什麼叫函數?咱們都知道js的全部運用和變化,都是基於做用域規則產生的,這個規則就是內部做用域擁有其所在的外部做用域的訪問權,這意味着內部做用域老是能夠拿到外部做用域聲明的變量,而外部做用域卻沒有內部做用域的直接訪問權。而每個函數被聲明時,就至關於創造了本身的做用域,同時也遵循做用域的規則。異步

與此同時,函數自己也遵循軟件開發的最小暴露原則,放在這裏理解的話,就是說當一個做用域內聲明瞭一個函數的時候,這個函數對於當前做用域來講,就是一個小黑屋,做用域並不知道里面有什麼,只有當這個函數被執行的時候,函數內部的邏輯對於做用域來講纔算是可見的。函數

以上指出的函數特色:工具

  1. 最小暴露原則。
  2. 創造做用域,並遵循做用域規則。

如今咱們來看看所謂的‘函數套函數’這個觀點:post

//全局做用域下寫了如下代碼
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
}
複製代碼

根據該觀點,咱們如今就至關於產生了一個閉包,看起來好像是這樣,但真的是麼? 當全局做用域中的邏輯被執行的時候,咱們遇到了一個函數的聲明,聲明瞭一個名叫outFun的函數,併產生了一個屬於outFun的局部做用域,很顯然,此時只是聲明瞭函數,而咱們沒有作任何其餘的事情,不要忘記剛說的最小暴露原則,那麼如今對於全局做用域來講,outFun就是一個小黑屋,裏面有什麼並不知道,也就是說對於全局做用域來講,outFun就是個普通的函數,與其餘的函數沒什麼差異,就更談不上閉包了。因此‘函數套函數就是閉包’這個觀點不是很靠譜。學習

閉包的產生時機

那麼問題來了,怎麼纔算是產生了閉包?或者說,閉包產生的時機又是什麼? 看這裏:

//全局做用域下寫了如下代碼
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
複製代碼

根據《你不知道的javascript》第五章第44頁中的定義--「當函數做用域能夠記住並訪問所在的詞法做用域時,就產生了閉包,不論函數是在當前詞法做用域外仍是內執行。」咱們來看上面這段代碼,從概念上來講產生了閉包(雖然是一個沒什麼做用的閉包),由於外部函數outFun的執行,聲明瞭內部函數innerFun,而函數innerFun在聲明的同時,記住並擁有了所在的詞法做用域的訪問權。

那麼能夠明確地一點是,閉包產生的時機是外部函數執行,內部函數聲明的時候。

爭議

事實上從概念的角度對閉包進行闡述是存在爭議的,此處感謝 @茹挺進 的不一樣思考。大部分觀點認爲下文中技術角度闡述的閉包纔算是真正的閉包,好比如下觀點:

  1. 簡單的說就是函數內部保存的變量不隨這個函數調用結束而被銷燬就算是產生了閉包。
  2. 可以完成信息隱藏,並進而應用於須要狀態表達的某些編程範型中的纔算是閉包。
  3. 閉包是由函數和與其相關的引用環境組合而成的實體。

可是徹底從技術角度來闡述閉包的話,就意味着:

  1. 閉包與內存泄露的產生綁定在了一塊兒(先不考慮後續對閉包的清理)。
  2. 同時,也再一次模糊了閉包產生的時機。

出於以上兩點的考慮,我進行了區分理解,與此同時,還存在一種狀況致使我決定將其區分,IIFE函數的存在,與概念上的閉包同樣,你說他是,他有不一樣,你說他不是,他有具有一部分特色,因此這裏你們能夠有更多的探討。

技術上的閉包

咱們上面說到示例代碼是一個沒什麼做用的閉包,由於閉包是一項用來解決實際問題的技術,那麼儘管上面的寫法在概念上來講算是閉包,可是從技術的角度來講,它又不是,由於它不能解決任何實際問題,那麼怎麼才能讓它變得有用呢,這就須要咱們想辦法讓內部函數變得可觀察,咱們來看下面的例子:

//全局做用域下寫了如下代碼
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
var newFun = outFun();
var res1 = newFun(1);
console.log(res1);   //1
var res2 = newFun(2);
console.log(res2);   //3
複製代碼

咱們經過return的方式,將內部函數的引用傳遞到了外面,同時又在外部函數所在的做用域中聲明瞭一個變量去引用它,使得內部函數變得可操做,也可觀察,從而製造了一個實用的閉包。

關於return的誤解

此時可能會有另外一些聲音出現,他們將目光聚焦到了return這個操做上,認爲閉包的標誌就是是否使用了return,將內部函數的引用傳遞出去,這是一個誤解,咱們用三個新的例子,來證實return的使用並非閉包所依賴的關鍵,它只是觀察和操做的手段之一。

example1:
//全局做用域下寫了如下代碼
const obj = {};
function outFun (){
    var a = 0;
    obj.newFun = function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
var res1 = obj.newFun(1);
console.log(res1);   //1
var res2 = obj.newFun(2);
console.log(res2);   //3
複製代碼
example2:
//全局做用域下寫了如下代碼
let newFun;
function outFun (){
    var a = 0;
    newFun = function innerFun(b){
        a+=b;
        return a;
    }
}
outFun();
var res1 = newFun(1);
console.log(res1);   //1
var res2 = newFun(2);
console.log(res2);   //3
複製代碼
example3:
//全局做用域下寫了如下代碼
let arr = [];
function outFun (){
    var a = 0;
    function innerFun(b){
        a+=b;
        return a;
    }
    arr.push(innerFun);
}
outFun();
var res1 = arr[0](1);
console.log(res1);   //1
var res2 = arr[0](2);
console.log(res2);   //3
複製代碼

例子中並無使用return,因此return跟閉包沒有任何關係,事實上將內部函數變爲可觀察、可操做的核心並不拘泥於某種形式,而是在於將函數的引用傳遞出去便可。

從這裏不難看出,閉包也包含了一種對引用關係的闡述。

真的內存泄露了嗎?

既然咱們已經明瞭了閉包的概念、產生及其意義,那麼不可或缺的也要面對它的一些其餘的特性,或者說問題---內存泄漏, 直白點說,內存泄漏就是指某一塊內存由於某種緣由沒法被釋放回收,從而形成可用內存出現缺失的情況。 至關一部分人習慣把閉包與內存泄漏捆綁在一塊兒,這個觀點是籠統的,咱們只能說,閉包可能形成內存泄漏。

前置說明---檢查內存泄漏的工具及使用方法:

傳送門:你覺得內存泄露怎麼偵測

咱們來看下面對比的兩個個例子:

//全局做用域下寫了如下代碼
//發生內存泄漏的例子
var obj = {};
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
setTimeout(function (){
    obj.newFunc = outFun();
    console.log(obj.newFunc(1)); //1
},3000);
setTimeout(function (){
    console.log(obj.newFunc(2)); // 3
},6000);
setTimeout(function (){
    obj.newFunc = null;
    console.log('clean'); //clean
},9000);
複製代碼
  1. 初始時內存快照,無變化,因此無內容
  2. 第一個settimeout以後,本次建立了閉包並佔用了內存
  3. 本次變化未處理閉包,因此閉包內存無變化,即該閉包內存未釋放
  4. 本次變化將引用閉包的對象屬性置空,閉包占用的內存被釋放
//全局做用域下寫了如下代碼
//未發生內存泄漏的例子
function outFun (){
    var a = 0;
    return function innerFun(b){
        a+=b;
        return a;
    }
}
setTimeout(function (){
    outFun();
    console.log('3秒後');
},3000);
複製代碼
  1. 初始時內存快照,無變化,因此無內容
  2. 第一個settimeout以後,outFun執行,本次建立了閉包,但執行完畢後內存當即被回收了,並無佔用內存

內存泄漏的真正緣由

從這裏咱們不難看出,閉包產生內存泄漏的一個必須的條件是被傳遞出去的內部函數的引用地址,是否被別的做用域的變量所引用。 只有知足這個條件,纔會發生內存泄漏,由於當函數outFun執行完畢後,js解釋器根據垃圾回收機制,要回收內存的時候,發現內部函數被其餘做用域所引用,而js解釋器並不知道這個被引用出去的內部函數啥時候執行,因此只能保持內存不釋放,以備隨時的使用。 因此,結論躍然紙上,閉包不表明就必定會發生內存泄漏,僅僅是可能發生閉包,好比開發者創造了閉包後,沒有及時清理。 與此同時,咱們能夠看出,閉包技術的特性是一把雙刃劍,因爲它能將做用域內存保持住,那麼開發者就能夠在後續對該做用域中能訪問到的那些個變量作進一步處理,但同時,若是不能合理處理閉包,那麼嚴重的內存泄漏將致使內存溢出,程序崩潰。

閉包的實用例子

簡單的再說兩個閉包的實用例子,

  1. bind方法已然是耳熟能詳,當咱們使用js去shim它的時候,就能夠用到閉包,而這個過程,也能夠被稱之爲柯理化,又或者稱之爲預參數,它們都是閉包的一種運用方式。

//這段代碼能夠類比一種場景,一個ul中有5個li,開發者收集到了5個li的節點後,for循環,經過addeventlistener分別給每個li綁定click事件,打印當前遍歷的索引,效果雷同。
for(var i=1;i<6;i++){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}
複製代碼

以上代碼咱們指望的效果是過1秒輸出1,過2秒輸出2,過3秒輸出3,過4秒輸出4,過5秒輸出5,然而結果是都輸出的6,由於var聲明的變量,在for循環所在的做用域中只有一個,後面的每一次賦值都會覆蓋前面的值(這個狀況自己跟異步不要緊,只是異步了以後,凸顯出了這個特色),當第五次循環完畢後,會再次自增1,變爲6,只是6不知足循環條件,循環中斷,而後打印出來的就都是6。 如今咱們利用閉包改造下,

for(var i=1;i<6;i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)
}
複製代碼

如今就獲得了咱們想要的結果,過1秒輸出1,過2秒輸出2,過3秒輸出3,過4秒輸出4,過5秒輸出5。 至於緣由,上面關於閉包的分析裏已經涵蓋。

解決問題的方法永遠不僅一個

事實上解決這個問題的方式不必定非要使用閉包,

for(let i=1;i<6;i++){
    setTimeout(function(){
        console.log(i);
    },i*1000);
}
複製代碼

let以代碼塊{}爲做用域邊界,劫持做用域,使得每個塊內的i都是惟一的,互不干涉。

除此以外,還有別的方法解決該問題,

for(var i=1;i<6;i++){
    setTimeout(function(j){
        console.log(j);
    },i*1000,i);
}
複製代碼

這個緣由詳見settimeout的使用方法,不作贅述。

關於IIFE函數

有必要補充一點,關於自執行函數,即IIFE函數的一些說明。 IIFE函數,雖然的確創造了閉包,可是因爲它自己並無建立外部做用域,因此從嚴格上來說,它不像是閉包。 (雖然IIFE函數也可以引用到括號所在做用域的變量,但那是因爲詞法做用域查找規則存在的緣由,這個規則只是閉包的一部分。) (剛說了,閉包產生於其聲明的時候和聲明的位置,而IIFE在其聲明的做用域內是被隱藏的、是找不到的,這也是其不像閉包的另外一個緣由。)但它能很多解決問題,這是最重要的

寫在最後

須要聲明的一點是,我不是一個教授者,我只是一個分享者、一個討論者、一個學習者,有不一樣的意見或新的想法,提出來,咱們一塊兒研究。分享的同時,並不僅是被分享者在學習進步,分享者亦是。

知識遍地,拾到了就是你的。

既然有用,不妨點贊,讓更多的人瞭解、學習並提高。

相關文章
相關標籤/搜索