如何簡單理解閉包(Closure)

以前一直對閉包這個概念模模糊糊的,網上也是長篇大論,如今經過本身瞭解和學習,總結了一下一些閉包知識點,寫得不對的地方能夠指出,你們互相學習.面試

先了解一些變量的做用域:markdown

變量的做用域包括兩種:全局變量和局部變量。閉包

全局變量:異步

var n = 999;//全局變量
function f1(){
  console.log(n);
}
f1();//999
複製代碼

局部變量:函數

function f1(){
  var n = 999;//局部變量
}
console.log(n);//n is not defined
複製代碼

1、簡單理解閉包

先看一下MDN關於閉包的定義:學習

一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一塊兒(或者說函數被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你能夠在一個內層函數中訪問到其外層函數的做用域。在 JavaScript 中,每當建立一個函數,閉包就會在函數建立的同時被建立出來。ui

重點的一句:閉包讓你能夠在一個內層函數中訪問到其外層函數的做用域。spa

如今不理解也不要緊,繼續往下看:code

學習一個概念時,最好的方法就是找它的demo,從demo中理解和分析,下面先看一段代碼,這是一個最簡單的閉包:orm

function f1(){
  var n = 999;
  
  function f2(){
    console.log(n);
  }
  
  return f2//返回內部函數f2,這樣在f1中就能讀取f2的數據和函數等價於window.f2 = f2;
  
}

var result = f1();
result();//999
複製代碼
  • 首先定義個普通函數f1;
  • 在f1中再定義一個普通函數f二、和在f1函數中的內部變量n;
  • 在f1中返回函數f2(確切說,在f1中返回了f2的引用);
  • 將f1的返回值賦值給變量result;
  • 執行result

在上邊的代碼中,f1函數裏面嵌套了一個函數f2,而且f2調用了f1的變量,那麼變量n和函數f2組合就成了一個閉包。

那爲何是閉包呢?咱們能夠根據上邊MDN對閉包的定義這句話(閉包讓你能夠在一個內層函數中訪問到其外層函數的做用域。)進行分析,咱們再看一張圖:

image.png

f1是一個外部函數,變量n是外部函數的局部變量,f2是嵌套在f1中的一個內部函數,在內部函數f2中調用了外部函數f1的變量n,因此f2和變量n就組成了一個閉包。

那麼,咱們就能夠得出產生閉包的條件:

  • 一個外部函數裏面嵌套着一個內部函數;好比外部函數f1裏面嵌套了一個內部函數f2
  • 一個嵌套的內部函數調用了外部函數的內部變量或函數;好比f2內部函數調用了外部函數f1的變量n

只要知足以上兩個條件,就產生了閉包。

那你可能會問爲何要return f1呢?

由於在JS中,只要內部函數纔可以讀取外部函數的內部變量或數據,反之則不行,若是你不return f2,那你將沒法使用f2這個閉包,return f2是爲了在f1中能使用f2的變量和數據,與閉包沒有關係的。

那到底什麼是閉包呢?

能夠通俗理解成:閉包就是有權訪問另外一個函數做用域中內部變量或數據的函數,由於在JS中,只要內部函數能可以讀取外部函數的變量或數據,反之就不行,全部能夠將閉包簡單理解成,定義在一個函數內部的函數。

總結:

閉包就是有權訪問另外一個函數內部變量的函數。

閉包產生的緣由:內部函數存在對外部函數局部變量的引用就會致使閉包。

到這裏相信你也已經對閉包有了一個簡單的瞭解了,可是單單是瞭解仍是不夠的,咱們學學習同樣技術,最重要的就是要學以至用,那咱們繼續往下了解吧。

2、閉包的經典使用場景

一、return一個內部函數,讀取內部函數的變量;

最大的一個用途就是前面提到的能夠:讀取內部函數的變量;

function f1(){
  var n = 999;
  function f2(){
    console.log(n);
  }
  return f2;
}

var result = f1();
result();//999
複製代碼

二、函數做爲參數

var n = 999;

function f1(){
  var n = 1000;
  function f2(){
    console.log(n);
  }
  return f2
}

function f3(p){
  var n = 1001;
  p();
}

f3(f1());//1000
複製代碼

三、IIFE(自執行函數)

var n = 999;
(function f1(){
  console.log(n);
})()
//999
複製代碼

上邊的代碼中f1( )是一個閉包,調用了全局變量n(即調用了window下的變量n);

四、循環賦值

for(var i = 0; i<10; i++){
  (function(j){
    setTimeout(function(){
      console.log(j);
    },1000)
  })(i)
}
//1,2,3,4,5,6,7,8,9,10依次打印
複製代碼

五、使用回調函數就是在使用閉包

window.n = 999;
setTimeout(function f1(){
  console.log(window.n);
},1000)
複製代碼

六、將外部函數建立的變量值始終保持在內存中;

能夠看下下面這段代碼:

function f1(){
  var n = 999;
  function f2(){
    console.log(n++);
  }
  result f2
}
var result = f1();
result();//1000
複製代碼

上邊代碼中f1的內部變量n一直存在內存中,不會在f1調用結束後被自動清除。 再看另外一段代碼:

function f1(){
  var n = 999;
  nAdd = function(){
    n+=1;
  }
  function f2(){
    console.log(n);
  }
  result f2
}

var result = f1();
result();//999
nAdd();
result();//1000
複製代碼

上邊代碼中函數f1的返回值賦值給了全局變量result,函數f1的返回值實際上就是f2函數,能夠理解爲f2被賦值給了全局變量result,這就致使了f2始終在內存中,而f2的存在依賴於f1,所以f1也始終在內存中,不會在調用結束以後,被垃圾回收機制(GC機制)回收,全部很容易形成內存泄漏

內存泄漏,就是一些你訪問不到或用不到的變量,還佔據着內存空間,不能被再次利用起來。

七、封裝對象的私有對象和私有方法;

var Counter = (function(){
   var privateCounter = 0;
   return function changeBy(val){
     privateCounter += val;
   }
   return {
     increment:function(){
       changeBy(1);
     },
     decrement:function(){
       changeBy(-1);
     },
     value:function(){
       return privateCounter;
      }
   }
 })();

console.log(Counter.value());//0
Counter.increment();
Counter.increment();
console.log(Counter.value());//2
Counter.decrement();
console.log(Counter.value());//1
複製代碼

3、 使用閉包須要注意什麼?

由於使用閉包會包含其餘函數的做用域,會比其餘函數佔據更多的內存空間,不會在調用結束以後被垃圾回收機制(簡稱GC機制)回收,多度使用閉包會過分佔用內存,形成內存泄漏。

4、閉包相關的面試題

一、簡述什麼是閉包,閉包的做用是什麼?寫出一個簡單的閉包例子。

二、閉包會形成內存泄漏嗎?

會,由於使用閉包會包含其餘函數的做用域,會比其餘函數佔據更多的內存空間,不會在調用結束以後被垃圾回收機制回收,多度使用閉包會過分佔用內存,形成內存泄漏。

三、for循環和閉包(必刷題)

var data = [];

for(var i = 0; i < 3; i++){
  data[i] = function (){
  console.log(i);
  };
}

data[0]();//3
data[1]();//3
data[2]();//3
複製代碼

上邊代碼的變量i屬於一個全局變量,公用一個做用域,全部輸出是3個3; 使用閉包改善上邊的寫法達到預期的效果:

var data = [];
for(var i = 0; i < 3; i++){
  (function(j){
    setTimeout(data[j] = function(){
    console.log(j);
    },0)
  })(i)
}
data[0]();
data[1]();
data[2]();
複製代碼

四、請寫出如下代碼的輸出結果:

第一題4-1:

var n = 10;
function f1(){
  var n = 20;
  function f2(){
    n++
    console.log(n);
  }
  f2();
  return f2
}

var result = f1();//21
result();//22
result();//23
console.log(n);//10
複製代碼

第二題4-2:

function makeAdder(x){
  return function(y){
    return x+y;
  }
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2));//7
console.log(add10(2));//12
複製代碼

第三題4-3:

var Counter = (function(){
   var privateCounter = 0;
   return function changeBy(val){
     privateCounter += val;
   }
   return {
     increment:function(){
       changeBy(1);
     },
     decrement:function(){
       changeBy(-1);
     },
     value:function(){
       return privateCounter;
      }
   }
 })();

console.log(Counter.value());//0
Counter.increment();
Counter.increment();
console.log(Counter.value());//2
Counter.decrement();
console.log(Counter.value());//1
複製代碼

第四題4-4:

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); //0
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); //2
Counter1.decrement();
console.log(Counter1.value()); //1
console.log(Counter2.value()); //0
複製代碼

Counter1和Counter2是兩個獨立的閉包,一個閉包變量的值改變不會影響到另外一個閉包的變量。

第五題4-5:

for(var i = 0; i < 10; i++){
  setTimeout(
    function(){
    console.log(i);
    },1000)
}
//10 10 10 10 10 10 10 10 10 10每隔1秒輸出10,一共10個10
複製代碼

由於setTimeout是異步的,for循環是同步的,同步代碼執行完,i已是10了,異步代碼纔開始執行,因此i最後打印的是10。

若是將var換成let,打印的結果也不同:

for(let i = 0; i < 10; i++){
  setTimeout(
    function(){
    console.log(i);
    },1000)
}//1,2,3,4,5,6,7,8,9,10
複製代碼

在for循環中使用var,那i就是一個全局變量,循環結束以後i的值爲10。

持續補充更新,記錄很差的地方望指出修改,共同進步~

天天都給本身打氣,今天也要加油鴨~

相關文章
相關標籤/搜索