這次把 javascript 閉包給你講的明明白白

目錄

引入(閉包和塊作用域)

理解閉包:

常見的閉包:

閉包的作用:

閉包的生命週期:

利用閉包的實際例子(返回價格的區間元素)

移動動畫的抖動和加速(閉包應用,動畫演示)

根據閉包進行傳入字段排序(通用排序)

閉包內存泄漏的解決方法

閉包導致的this遺留問題(教你判斷this指向口訣)

閉包的其他應用 : 定義JS模塊


 

點贊再看,養成好習慣,總結不易,老鐵多多支持~

 

引入(閉包和塊作用域)

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>00_引入</title>
  </head>

  <body>

    <button>測試1</button>
    <button>測試2</button>
    <button>測試3</button>
    <!--
需求: 點擊某個按鈕, 提示"點擊的是第n個按鈕"
-->
    <script type="text/javascript">
      var btns = document.getElementsByTagName('button');
      //遍歷加監聽
      /*
      for (var i = 0,length=btns.length; i < length; i++) {
        var btn = btns[i]
        btn.onclick = function () {
          alert('第'+(i+1)+'個'); // 永遠是第btns.length個,因爲for循環已結束
        }
      }*/
      // 方法一,利用對象屬性
      /*
      for (var i = 0,length=btns.length; i < length; i++) {
        var btn = btns[i]
        //將btn所對應的下標保存在btn上
        btn.index = i
        btn.onclick = function () {
          alert('第'+(this.index+1)+'個')
        }
      }*/

      // 方法二,利用閉包
      // for (var i = 0, length = btns.length; i < length; i++) {
      //   // (function (j) {
      //   //  var btn = btns[j]
      //   //  btn.onclick = function () {
      //   //    alert('第' + (j + 1) + '個')
      //   //  }
      //   // })(i);
      //   // 等同於上面一段
      //   (function () {
      //     var j = i; // 因爲當前作用域是沒有i的,i在作用域之外,這樣onclick就持有了該作用域的引用,這個引用就叫做閉包
      //     var btn = btns[j];
      //     btn.onclick = function () {
      //       alert('第' + (j + 1) + '個');
      //     }
      //   })();
      // }

      // 或者利用ES6的let聲明和塊作用域結合起來
      // for循環頭部的let聲明會在每次循環迭代的過程中被聲明
      for (let i = 0, length = btns.length; i < length; i++) {
        let btn = btns[i];
        btn.onclick = function () {
          alert('第' + (i + 1) + '個'); // 持有外部作用域的引用,形成閉包
        }
      }
      // 塊作用域和閉包聯手便可以天下無敵
    </script>
  </body>

</html>

運行效果:

理解閉包:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>01_理解閉包</title>
  </head>

  <body>
    <!--
1. 如何產生閉包?
  * 當一個嵌套的內部(子)函數引用了嵌套的外部(父)函數的變量(函數)時, 就產生了閉包
2. 閉包到底是什麼?
  * 使用chrome調試查看
  * 理解一: 閉包是嵌套的內部函數(絕大部分人)
  * 理解二: 包含被引用變量(函數)的對象(極少數人)
  * 注意: 閉包存在於嵌套的內部函數中
3. 產生閉包的條件?
  * 函數嵌套
  * 內部函數引用了外部函數的數據(變量/函數)
-->
    <script type="text/javascript">
      function fn1() {
        var a = 2;
        var b = 'abc';
        function fn2() { //執行函數定義就會產生閉包(不用調用內部函數)
          console.log(a);
        }
        // fn2();
      }
      fn1();

      function fun1() {
        var a = 3;
        var fun2 = function () {
          console.log(a);
        }
      }
      fun1();
    </script>
  </body>

</html>

 

常見的閉包:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>02_常見的閉包</title>

  </head>

  <body>
    <!--
1. 將函數作爲另一個函數的返回值
2. 將函數作爲實參傳遞給另一個函數調用
-->
    <script type="text/javascript">
      // 1. 將函數作爲另一個函數的返回值
      function fn1() {
        var a = 2
        function fn2() {
          a++
          console.log(a)
        }
        return fn2
      }
      var f = fn1()
      f() // 3
      f() // 4

      // 2. 將函數作爲實參傳遞給另一個函數調用
      function showDelay(msg, time) {
        setTimeout(function () {
          alert(msg)
        }, time)
      }
      showDelay('atguigu', 2000)

    </script>
  </body>

</html>

 

閉包的作用:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>03_閉包的作用</title>

  </head>

  <body>
    <!--
1. 使用函數內部的變量在函數執行完後, 仍然存活在內存中(延長了局部變量的生命週期)
2. 讓函數外部可以操作(讀寫)到函數內部的數據(變量/函數)

問題:
  1. 函數執行完後, 函數內部聲明的局部變量是否還存在?  一般是不存在, 存在於閉中的變量纔可能存在
  2. 在函數外部能直接訪問函數內部的局部變量嗎? 不能, 但我們可以通過閉包讓外部操作它
-->
    <script type="text/javascript">
      function fn1() {
        var a = 2
        function fn2() {
          a++
          console.log(a)
          // return a
        }
        function fn3() {
          a--
          console.log(a)
        }
        return fn3
      }
      var f = fn1()
      f() // 1
      f() // 0
    </script>

  </body>

</html>

 

 

閉包的生命週期:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>04_閉包的生命週期</title>

  </head>

  <body>
    <!--
1. 產生: 在嵌套內部函數定義執行完時就產生了(不是在調用)
2. 死亡: 在嵌套的內部函數成爲垃圾對象時
-->
    <script type="text/javascript">
      function fn1() {
        //此時閉包就已經產生了(函數提升, 內部函數對象已經創建了)
        var a = 2
        function fn2() {
          a++
          console.log(a)
        }
        return fn2
      }
      var f = fn1()
      f() // 3
      f() // 4
      f = null //閉包死亡(包含閉包的函數對象成爲垃圾對象)
    </script>
  </body>

</html>

利用閉包的實際例子(返回價格的區間元素)

let lessons = [
  {
    title: "媒體查詢響應式佈局",
    click: 89,
    price: 12
  },
  {
    title: "FLEX 彈性盒模型",
    click: 45,
    price: 120
  },
  {
    title: "GRID 柵格系統",
    click: 19,
    price: 67
  },
  {
    title: "盒子模型詳解",
    click: 29,
    price: 300
  }
];
function between(a, b) {
  return function(v) {
	return v.price >= a && v.price <= b;
  }
}
console.table(lessons.filter(between(10, 100)));

執行結果

移動動畫的抖動和加速(閉包應用,動畫演示)

<!DOCTYPE html>
<html lang="en">
    <body>
        <style>
            button {
                position: absolute;
            }
        </style>
        <button>按鈕</button>
    </body>
    <script>
        let btns = document.querySelectorAll("button");
        btns.forEach(function (item) {
            let bind = false;
            item.addEventListener("click", function () {
                // if (!bind) {
                let left = 1;
                bind = setInterval(function () {
                    item.style.left = left++ + "px";
                }, 100);
                // }
            });
        });
    </script>
</html>

運行結果

這居然?鬼畜了???

就是因爲你的left寫在了click回調函數裏面。因爲每點擊一次就會創建一塊function空間,裏面left變量去定時改變style,每改變一次style.left就會導致一次迴流從而再渲染一次。每次點擊left初始值爲1,上一次的已經爲+了很多次,上上次的已經爲+了非常多次。渲染的時候你就會看到一會1px一會很多px的鬼畜情況,也就是動畫抖動。

那麼可以把left變量提到click上面一行就解決了吧?

......   
            let bind = false;
            let left = 1;
            item.addEventListener("click", function () {
                // if (!bind) {
                bind = setInterval(function () {
                    item.style.left = left++ + "px";
                }, 100);
                // }
            });
......

 運行結果

這???居然加速了 ,越來越快了!!!因爲每點擊一次就會有一個定時器100ms輪詢改變left變量,這個left變量對於click回調函數來說是的共有的一塊作用域。所以越來越多的定時器不斷的left++,你就看到了加速現象。

正確做法如下:

<!DOCTYPE html>
<html lang="en">
    <body>
        <style>
            button {
                position: absolute;
            }
        </style>
        <button>按鈕</button>
    </body>
    <script>
        let btns = document.querySelectorAll("button");
        btns.forEach(function (item) {
            let bind = false;
            item.addEventListener("click", function () {
                if (!bind) {
                    let left = 1;
                    bind = setInterval(function () {
                        item.style.left = left++ + "px";
                    }, 100);
                }
            });
        });
    </script>
</html>

 

現象就正常了,沒有抖動也沒有加速。

 

根據閉包進行傳入字段排序(通用排序)

    <script>
        let lessons = [
            {
                title: "媒體查詢響應式佈局",
                click: 89,
                price: 12
            },
            {
                title: "FLEX 彈性盒模型",
                click: 45,
                price: 120
            },
            {
                title: "GRID 柵格系統",
                click: 19,
                price: 67
            },
            {
                title: "盒子模型詳解",
                click: 29,
                price: 300
            }
        ];
        function order(field, type = 'asc') { // 默認asc升序
            return (a, b) => {
                if (type == "asc") return a[field] > b[field] ? 1 : -1;
                return a[field] > b[field] ? -1 : 1;
            }
        }
        console.table(lessons.sort(order("price"))); // order("price", "desc")可以降序
    </script>

 

閉包內存泄漏的解決方法

閉包特性中上級作用域會爲函數保存數據,從而造成的如下所示的內存泄漏問題

<!DOCTYPE html>
<html lang="en">
    <body>
        <div desc="zaixianxuexi">在線學習</div>
        <div desc="kaiyuanchanpin">開源產品</div>
    </body>
    <script>
        let divs = document.querySelectorAll("div");
        divs.forEach(function (item) {
            item.addEventListener("click", function () {
                console.log(item.getAttribute("desc"));
            });
        });
    </script>
</html>

下面通過清除不需要的數據解決內存泄漏問題

let divs = document.querySelectorAll("div");
divs.forEach(function(item) {
  let desc = item.getAttribute("desc");
  item.addEventListener("click", function() {
    console.log(desc);
  });
  item = null;
});

再給一個例子加深印象

    <script type="text/javascript">
      function fn1() {
        var arr = new Array[100000]
        function fn2() {
          console.log(arr.length)
        }
        return fn2
      }
      var f = fn1()
      f()

      f = null //讓內部函數成爲垃圾對象-->回收閉包
    </script>

 

閉包導致的this遺留問題(教你判斷this指向口訣)

let hd = {
  user: "後盾人",
  get: function() {
    console.log(this); // 這裏的this是hd對象
    return function() { // 這裏的this是window對象
      return this.user;
    };
  }
};
var a = hd.get(); // 執行get(),返回裏面的function
// this 總是指向調用該函數的對象,即函數在搜索this時只會搜索到當前活動對象。

// 下面是函數因爲是在全局環境下調用的,所以this指向window
console.log(a()); // this.user爲undefined,因爲this指向window

this 總是指向調用該函數的對象,即函數在搜索this時只會搜索到當前活動對象。所以這裏get()裏面的function的this是Window。

順帶總結一下this記憶方法

如果你定義的是對象,對象裏面定義了函數,這個函數叫做方法,方法裏面的this是指向當前對象,如果方法裏面還有函數2,那麼函數2的this指向Window,理由就是上面這個例子。

如果你定義的函數,而你執行的時候是new 函數,那麼認爲你創建了對象,this判斷同上。

如果你定義的函數,執行的時候直接調用,比如function a(){...},調用爲a(),那麼a裏面的this指向Window

上面犀利的總結一般人我不告訴他。

 

閉包的其他應用 : 定義JS模塊

閉包的自定義js模塊:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>05_閉包的應用_自定義JS模塊</title>
</head>
<body>
<!--
閉包的應用2 : 定義JS模塊
  * 具有特定功能的js文件
  * 將所有的數據和功能都封裝在一個函數內部(私有的)
  * 只向外暴露一個包信n個方法的對象或函數
  * 模塊的使用者, 只需要通過模塊暴露的對象調用方法來實現對應的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
  var module = myModule()
  module.doSomething()
  module.doOtherthing()
</script>
</body>
</html>

 

myModule.js

function myModule() {
  //私有數據
  var msg = 'My atguigu'
  //操作數據的函數
  function doSomething() {
    console.log('doSomething() ' + msg.toUpperCase())
  }
  function doOtherthing() {
    console.log('doOtherthing() ' + msg.toLowerCase())
  }

  //向外暴露對象(給外部使用的方法)
  return {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
}

 

閉包的自定義js模塊2:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <title>05_閉包的應用_自定義JS模塊2</title>
  </head>

  <body>
    <!--
閉包的應用2 : 定義JS模塊
  * 具有特定功能的js文件
  * 將所有的數據和功能都封裝在一個函數內部(私有的)
  * 只向外暴露一個包信n個方法的對象或函數
  * 模塊的使用者, 只需要通過模塊暴露的對象調用方法來實現對應的功能
-->
    <script type="text/javascript" src="myModule2.js"></script>
    <script type="text/javascript">
      myModule2.doSomething()
      myModule2.doOtherthing()
    </script>
  </body>

</html>

myModule2.js

(function () {
  //私有數據
  var msg = 'My atguigu'
  //操作數據的函數
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露對象(給外部使用的方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})()

這個應用還有新玩法,見這裏: js除了立即執行函數,你還可以這麼玩 (預計閱讀 1 min)

 

 

關注、留言,我們一起學習。

 

===============Talk is cheap, show me the code================

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