今天呢我們來聊聊這個js閉包,咱們基本上在面試中,必然會問到的問題:什麼是閉包?說說你對閉包的理解.閉包的做用是什麼?javascript
閉包也是一個很很差理解的概念,每每咱們遇到的機會不少不少,不少朋友呢都說了對閉包的理解,問題表達的方式不同,可是呢,最後都對閉包沒有很清晰的理解.因此呢我這邊就幫助你們理解什麼是閉包.其實提及來,能夠深,也能夠淺.先由淺着說.以前呢,在網上也是找了很多的資料,看見人家理解的閉包,我提取出了說法有問題 的4點:java
1.閉包是指有權訪問另外一個函數做用域中變量(參數)的函數(不可取)面試
2.閉包就是能讀取其餘函數內部變量的函數(不可取)瀏覽器
3.閉包能夠理解成定義一個函數內部的函數(不可取)閉包
4.函數就是閉包(不可取)ide
這4點呢,其實呢,怎麼說呢,不可否認它是錯的,只能說不嚴謹,第一點,能夠獲得一個結論,閉包是一個函數,第二點也差很少的意思,第三點有意思了,定義一個函數內部的函數,的確有這個特徵,而第四點,其實也是對的,由於MDN上的解釋是:閉包是一個特殊的函數對象.那上面的幾種說法都是不嚴謹的,其實最終我查詢資料,都概括了一句話:模塊化
當一個函數可以記住並訪問到其所在的詞法做用域及做用域鏈,特別強調是在其定義的做用域外進行的訪問,此時該函數和其上層執行上下文共同構成閉包函數
怎麼理解這句話呢,其中包含兩個新的名詞,詞法做用域及做用域鏈,這個咱們擴展再說,這裏咱們直接當作做用域就好了,因此這裏就能夠知道,閉包確定是跟做用域有關的,並且做用域仍是很大的.而後下一句是在其定義的做用域外進行的訪問.這是上面4點都沒有講到的.而後再就是該函數,也就是內部函數和其上層執行的上下文,共同構成閉包,因此閉包是一個總體.說了這麼多仍是很抽象,那須要明確下列幾點:this
1.閉包必定是函數對象.net
2.函數內保持對上層做用域的引用
3.閉包和詞法做用域,做用域鏈,垃圾回收機制等息息相關
4.當函數在其定義的做用域外進行訪問時,才產生閉包
5.閉包是由該函數和其上層執行上下文共同構成
//閉包函數對象 function fn() { var a = 3; return function () { return ++a;//引用了fn下的a 保持對變量a的引用(上層做用域)的引用 } } //fn和其中的匿名函數共同構成 var res=fn(); res();//在外部進行訪問,造成閉包
上述標重點的纔是閉包的必要條件.因此呢,往後面試官問起,什麼閉包,我能夠這樣說:咱們一般所用的閉包是函數嵌套函數的形式,內部的那個函數經過return出來,而後內部的函數又保持對上層做用域的引用,並且內部函數還必需要在外部調用,這個時候整個結構,內部外部結構,整個就造成了閉包 這裏僅僅只是說的閉包的概念和理解,那具體有什麼做用呢,上代碼先
function Fn(){ var i=0; i++; alert(i); } Fn();//1 Fn();//1 Fn();//1
var i=10;//並不會污染函數內部的變量 function outerFn() { var i = 0; return function () { i++; console.log(i); } } var res = outerFn();//res指向返回的函數 res();//1 res();//2 res();//3 //瀏覽器運行結果能夠得出結論:能夠操做outerFn函數內部的變量
這兩段代碼對比一下,第一段代碼由於做用域和函數生命週期的關係,定義在fn函數中的變量i在調用完畢以後,就被垃圾回收機制給回收了,因此每次調用都是等於1,而第二段代碼,我們能夠在函數外包操做outerFn函數內部的變量,因此每次調用執行res(),都至關於改變了i的值,並無被垃圾回收機制給回收.這2個示例體現了什麼呢,就是咱們要講的閉包的2點應用
1.在函數外讀取函數內部的變量(避免全局變量的污染)
2.讓局部變量的值可以被保存下來(可讓變量常駐內存)
3.將模塊的公有屬性和方法暴露出來
第三點應用是下面要講的,爲的是把閉包的做用總結在一塊兒,方便你們理解,那我們來看第三點,體如今什麼地方呢
//模塊化寫法 var moduleB = (function () { var num = 100;//私有屬性 var rem = 200;//公有屬性 function add() {//公有方法 num++; console.log(num); } function divide() {//公有方法 num = num / 10; console.log(num); } function show(){ //私有方法 console.log(num); } //將須要暴露出去的屬性和方法return出去 return { add: add, divide: divide, rem: rem } })(); moduleB.add();//101 moduleB.divide();//10.1 moduleB.show();//moduleB.show is not a function console.log(moduleB.rem);//200
那若是問你,我們平時閉包的應用還有哪些呢,不少人說用的不多,乃至沒有,其實閉包離咱們很近,只是咱們沒有去發現它是閉包而已.好比不少事實我們習慣將js代碼寫在頭部,那就確定會寫一個頁面文檔加載的事件,來,看代碼說話:
window.onload=function(){ var oDiv=document.querySelector("div"); oDiv.onclick=function(){ alert(123); } }
這段代碼我們是否是很熟悉,是否是很普通,這代碼是基本天天都寫得,我能夠確定的告訴你們,這裏我們就寫了個閉包!說是閉包,那符合我們上面說的閉包的3點條件嗎,3點條件你們再回頭看一下:
1.函數內保持對上層做用域的引用
2.當函數在其定義的做用域外進行訪問時,才產生閉包
3.閉包是由該函數和其上層執行上下文共同構成
那問題來了有人說第一點就不符合,那裏來的對上層做用域的引用呢?其實你們忘了,這個onclick事件函數內部有一個隱式的引用,無論用不用,它就在那裏,沒錯,就是this,這個this的指向是oDiv,而oDiv的確是上層做用域的變量.那第二點,當函數在其定義的做用域外進行訪問,產生閉包,我們的onclick事件是否是全局事件啊,觸發這個事件也是至關於在全局調用了.第三點,onload事件和onclick的事件,這一個總體,是否是也符合了我們的內部函數和其上層執行上下文共同構成的條件.全部呢,閉包是否是離我們很近,其實這個函數會在之前的低版本IE上,會有很大的問題,由於閉包可讓變量常駐內存,會形成內存泄露,如今高版本瀏覽器會有它本身的釋放方法.若是讓我們本身釋放怎麼釋放呢
釋放內存:
//閉包會形成內存泄露的問題,因此頁面在解構的時候,直接清除 window.onunload = function () { oDiv = null; oDiv.onclick=null; }
還有一個應用,好比頁面上的ul標籤,裏面有不少個li,我們在獲取當前點擊的li的下標的時候,運用閉包會遇到,有人說用什麼閉包啊,事件委託啊,可是事件委託真的能夠獲取下標嗎,答案是不能夠的.看代碼:
var oLis = document.querySelectorAll("ul li"); for (var i = 0; i < oLis.length; i++) { //自執行函數 (function (i) { // i形參 oLis[i].onclick = function () { console.log(i);//點擊li,輸出當前li的下標 } })(i);//實參 } //閉包會形成內存泄露的問題,因此頁面在解構的時候,直接清除 window.onunload = function () { oLis = null; }
這也是一個閉包.因此呢,咱們是常常在寫閉包的,只不過是本身沒有注意罷了,之後面試別說本身沒有寫過閉包,由於這是不可能的.經過上面咱們對閉包的探究,那能夠給你們總結一下,閉包就是能夠建立一個獨立的環境,每一個閉包裏面的環境都是獨立的,互不干擾。閉包會發生內存泄漏,每次外部函數執行的時 候,外部函數的引用地址不一樣,都會從新建立一個新的地址。但凡是當前活動對象中有被內部子集引用的數據,那麼這個時候,這個數據不刪除,保留一根指針給內部活動對象。下面幾個閉包的例子,你們能夠看一下,仔細琢磨代碼:
注意,此示例和結論引用 https://blog.csdn.net/weixin_43586120/article/details/89456183 的示例,裏面示例更多,有興趣能夠去琢磨,看完下面示例以後,能夠帶入這個結論,說的很正確.結論閉包找到的是同一地址中父級函數中對應變量最終的值
function outerFn(){ var i = 0; function innerFn(){ i++; console.log(i); } return innerFn; } var inner = outerFn(); //每次外部函數執行的時候,外部函數的地址不一樣,都會從新建立一個新的地址 inner(); inner(); inner(); var inner2 = outerFn();//從新建立了一個新的引用地址 inner2(); inner2(); inner2() //結果是 1 2 3 1 2 3
var i = 0; function outerFn(){ function innnerFn(){ i++; console.log(i); } return innnerFn; } var inner1 = outerFn(); var inner2 = outerFn(); inner1(); inner2(); inner1(); inner2(); //結果是 1 2 3 4 i是全局的變量,改的是全局變量
(function() { var m = 0; function getM() { return m; } function seta(val) { m = val; } window.g = getM; window.f = seta; })(); f(100); console.info(g()); //100 閉包找到的是同一地址中父級函數中對應變量最終的值
function love1(){ var num = 223; var me1 = function() { console.log(num); } num++; return me1; } var loveme1 = love1(); loveme1(); //輸出224 這裏做用域內var變量進行了聲明提高,在累加以前,函數未調用
擴展:
上面呢,我們說了兩個概念,詞法做用域和做用域鏈,做用域鏈呢我們這裏不講,挖的深了就不少了,不易理解,我這裏簡單的擴展下詞法做用域,詞法做用域也叫靜態做用域,它的做用域是指在詞法分析階段就肯定了,不會改變,我們的JavaScript就是使用的詞法做用域.看個例子:
//詞法做用域 var abc=1;//全局 function fn(){ console.log(abc);//這裏註定是調用的全局的變量abc } function fx(){ var abc=2;//fn函數沒法訪問到這裏的abc fn(); } fx();
看上面,很普通的一段代碼,我們上面講了閉包,確定不少人覺得這個答案會輸出2,但其實我告訴你,這個答案是輸出1,爲何呢,這個就跟詞法做用域有關.詞法做用域(靜態做用域)有很重要的一條,詞法做用域關注函數在何處聲明.好好想一想,第一個abc是全局的,下面函數fn是直接打印的abc,那你認爲fn中的abc會去能訪問到fx函數中的變量a嗎,確定不是.固然了,若是你給fn函數把abc當作一個形參傳遞,那確定是能夠訪問到abc的值的,會打印2.,這就是詞法做用域,關注函數在何處聲明.
那好,既然有靜態做用域,那確定有動態的做用域,
動態做用域是在運行時根據程序的流程信息來動態肯定的,而不是在寫代碼時靜態肯定的.那說到這裏,我們js中有那塊很是相似這個動態做用域呢?沒錯,就是this,上代碼,看着更直觀點
function fx(){ console.log(this);//根據調用fx狀況來指向哪裏 } fx();//此時this 是指向window的 //頁面上有個按鈕,獲取btn oBtn.onclick=function(){ console.log(this);//這裏的this就是指向按鈕oBtn了 fx(); } //這個就是相似的動態做用域
動態做用域關注的是函數從何處調用,詞法做用域和動態做用域的主要區別就是:詞法做用域是在寫代碼或者定時肯定的,而動態做用域是在運行時肯定的.
好了,閉包的基本知識就講到這裏了,這是從淺處去挖,尚未到深處,各位大神有興趣,能夠評論區指教下,後面的詞法做用域和動態做用域,當作是擴展的了,但願你們能明白.