js--閉包與垃圾回收機制

丶Serendipity丶發表於2021-02-28

前言

  閉包和垃圾回收機制常常作為前端學習開發中的難點,也經常在面試中遇到這樣的問題,本文記錄一下在學習工作中關於這方面的筆記。

正文

 1.閉包

  閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高階應用都要依靠閉包實現。作為一個JavaScript開發者,理解閉包十分重要。

  1.1閉包是什麼?

  閉包就是一個函式引用另一個函式的變數,內部函式被返回到外部並儲存時產生,(內部函式的作用域鏈AO使用了外層函式的AO)

       因為變數被引用著所以不會被回收,因此可以用來封裝一個私有變數,但是不必要的閉包只會增加記憶體消耗。

  閉包是一種保護私有變數的機制,在函式執行時形成私有的作用域,保護裡面的私有變數不受外界干擾。或者說閉包就是子函式可以使用父函式的區域性變數,還有父函式的引數。

  1.2閉包的特性

   ①函式巢狀函式

  ②函式內部可以引用函式外部的引數和變數

  ③引數和變數不會被垃圾回收機制回收

  1.3理解閉包

  基於我們所熟悉的作用域鏈相關知識,我們來看下關於計數器的問題,如何實現一個函式,每次呼叫該函式時候計數器加一。

    var counter=0;
    function demo3(){
        console.log(counter+=1);       
    }
    demo3();//1
    demo3();//2
    var counter=5;
    demo3();  //6
  上面的方法,如果在任何一個地方改變counter的值 計數器都會失效,javascript解決這種問題用到閉包,就是函式內部內嵌函式,再來看下利用閉包如何實現。
     function add() {
            var counter = 0;
            return function plus() {
                counter += 1;
                return counter
            }      
        }
        var count=add()
        console.log(count())//1
        var counter=100
        console.log(count())//2

  上面就是一個閉包使用的例項 ,函式add內部內嵌一個plus函式,count變數引用該返回的函式,每次外部函式add執行的時候都會開闢一塊記憶體空間,外部函式的地址不同,都會重新建立一個新的地址,把plus函式巢狀在add函式內部,這樣就產生了counter這個區域性變數,內次呼叫count函式,該區域性變數值加一,從而實現了真正的計數器問題。

  1.4閉包的主要實現形式

  這裡主要通過兩種形式來學習閉包:

  ①函式作為返回值,也就是上面的例子中用到的。

        function showName(){
                var name="xiaoming"
                return function(){
                    return name
                }
            }
            var name1=showName()
            console.log(name1())    

  閉包就是能夠讀取其他函式內部變數的函式。閉包就是將函式內部和函式外部連線起來的一座橋樑。

  ②閉包作為引數傳遞

        var num = 15
            var foo = function(x){
                if(x>num){
                    console.log(x)
                }  
            }
            function foo2(fnc){
                var num=30
                fnc(25)
            }
            foo2(foo)//25

上面這段程式碼中,函式foo作為引數傳入到函式foo2中,在執行foo2的時候,25作為引數傳入foo中,這時判斷的x>num的num取值是建立函式的作用域中的num,即全域性的num,而不是foo2內部的num,因此列印出了25。

  1.5閉包的優缺點

  優點:

  ①保護函式內的變數安全 ,實現封裝,防止變數流入其他環境發生命名衝突

  ②在記憶體中維持一個變數,可以做快取(但使用多了同時也是一項缺點,消耗記憶體)

  ③匿名自執行函式可以減少記憶體消耗

  缺點:

  ①其中一點上面已經有體現了,就是被引用的私有變數不能被銷燬,增大了記憶體消耗,造成記憶體洩漏,解決方法是可以在使用完變數後手動為它賦值為null;

  ②其次由於閉包涉及跨域訪問,所以會導致效能損失,我們可以通過把跨作用域變數儲存在區域性變數中,然後直接訪問區域性變數,來減輕對執行速度的影響。

  1.6閉包的使用

  for (var i = 0; i < 5; i++) {
      setTimeout(function() {
          console.log( i);
      }, 1000);
  }

  console.log(i);

  我們來看上面的問題,這是一道很常見的題,可這道題會輸出什麼,一般人都知道輸出結果是 5,5,5,5,5,5,你仔細觀察可能會發現這道題還有很多巧妙之處,這6個5的輸出順序具體是怎樣的?5 -> 5,5,5,5,5 ,瞭解同步非同步的人也不難理解這種情況,基於上面的問題,接下來思考如何實現5 -> 0,1,2,3,4這樣的順序輸出呢?

    for (var i = 0; i < 5; i++) {
        (function(j) {  // j = i
            setTimeout(function() {
                console.log( j);
            }, 1000);
        })(i);
    }
    console.log( i);
//5 -> 0,1,2,3,4    

  這樣在for迴圈種加入匿名函式,匿名函式入參是每次的i的值,在同步函式輸出5的一秒之後,繼續輸出01234。

    for (var i = 0; i < 5; i++) {
        setTimeout(function(j) {
            console.log(j);
        }, 1000, i);
    }
    console.log( i);
    //5 -> 0,1,2,3,4

  仔細檢視setTimeout的api你會發現它還有第三個引數,這樣就省去了通過匿名函式傳入i的問題。

  var output = function (i) {
      setTimeout(function() {
          console.log(i);
      }, 1000);
  };

  for (var i = 0; i < 5; i++) {
      output(i);  // 這裡傳過去的 i 值被複制了
  }

  console.log(i);
  //5 -> 0,1,2,3,4

  這裡就是利用閉包將函式表示式作為引數傳遞到for迴圈中,同樣實現了上述效果。

  for (let i = 0; i < 5; i++) {
      setTimeout(function() {
          console.log(new Date, i);
      }, 1000);
  }
  console.log(new Date, i);
  //5 -> 0,1,2,3,4

  知道let塊級作用域的人會想到上面的方法。但是如果要實現0 -> 1 -> 2 -> 3 -> 4 -> 5這樣的效果呢。

  for (var i = 0; i < 5; i++) {
      (function(j) {
          setTimeout(function() {
              console.log(new Date, j);
          }, 1000 * j);  // 這裡修改 0~4 的定時器時間
      })(i);
  }

  setTimeout(function() { // 這裡增加定時器,超時設定為 5 秒
      console.log(new Date, i);
  }, 1000 * i);
  //0 -> 1 -> 2 -> 3 -> 4 -> 5

  還有下面的程式碼,通過promise來實現。

  const tasks = [];
  for (var i = 0; i < 5; i++) {   // 這裡 i 的宣告不能改成 let,如果要改該怎麼做?
      ((j) => {
          tasks.push(new Promise((resolve) => {
              setTimeout(() => {
                  console.log(new Date, j);
                  resolve();  // 這裡一定要 resolve,否則程式碼不會按預期 work
              }, 1000 * j);   // 定時器的超時時間逐步增加
          }));
      })(i);
  }

  Promise.all(tasks).then(() => {
      setTimeout(() => {
          console.log(new Date, i);
      }, 1000);   // 注意這裡只需要把超時設定為 1 秒
  });
  //0 -> 1 -> 2 -> 3 -> 4 -> 5
  const tasks = []; // 這裡存放非同步操作的 Promise
  const output = (i) => new Promise((resolve) => {
      setTimeout(() => {
          console.log(new Date, i);
          resolve();
      }, 1000 * i);
  });

  // 生成全部的非同步操作
  for (var i = 0; i < 5; i++) {
      tasks.push(output(i));
  }

  // 非同步操作完成之後,輸出最後的 i
  Promise.all(tasks).then(() => {
      setTimeout(() => {
          console.log(new Date, i);
      }, 1000);
  });
  //0 -> 1 -> 2 -> 3 -> 4 -> 5
// 模擬其他語言中的 sleep,實際上可以是任何非同步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 宣告即執行的 async 函式表示式
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
//0 -> 1 -> 2 -> 3 -> 4 -> 5

  上面的程式碼中都用到了閉包,總之,閉包找到的是同一地址中父級函式中對應變數最終的值。

  2.垃圾回收機制

 

  JavaScript 中的記憶體管理是自動執行的,而且是不可見的。我們建立基本型別、物件、函式……所有這些都需要記憶體。

  通常用採用的垃圾回收有兩種方法:標記清除、引用計數。

  1、標記清除

  垃圾收集器在執行的時候會給儲存在記憶體中的所有變數都加上標記。然後,它會去掉環境中的變數以及被環境中的變數引用的標記。

  而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變數了。

  最後。垃圾收集器完成記憶體清除工作,銷燬那些帶標記的值,並回收他們所佔用的記憶體空間

  2.引用計數

  引用計數的含義是跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就是1。

  相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,

  則說明沒有辦法再訪問這個值了,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再執行時,

  它就會釋放那些引用次數為0的值所佔的記憶體。

 

總結

  以上就是本文的全部內容,希望給讀者帶來些許的幫助和進步,方便的話點個關注,小白的成長之路會持續更新一些工作中常見的問題和技術點。

 

相關文章