前端小知識--從Javascript閉包看let

JiaXinYi發表於2018-03-19

let和閉包

之前一直模模糊糊記得,let解決了某個閉包問題,想用時又不敢肯定,今天終於遇到這個問題了,那我們就一起來分析一下,什麼是let,let有什麼作用,以及,他是如何解決閉包的,當然,也順便好好聊聊閉包。

1、閉包

1.1 閉包的定義

閉包的定義是這樣的:內部函式被儲存到了外部,即為閉包
先來看一個簡單的例子:

//我們宣告一個函式test(),這個函式返回了一個function
 function test(){
     var i = 0;
     return function(){
        console.log(i++)
     }
 }; 
//把test()的返回值賦給a和b變數,所以其實這時候的a/b=function(){console.log(i++);}
 var a = test();
 var b = test();
 //依次執行a,a,b,控制檯會輸出什麼呢?
 a();a();b();

先思考一下,然後去瀏覽器驗證一下

答案是:0,1,0

clipboard.png

這是因為,a/b=test()時,a/b各自保留了test的AO,所以各自上面均有一個i=0;

1.2 實現共有變數

這樣其實是實現了一個共有變數,比如我們把上面的程式碼稍稍調整一下,就實現了一個累加計數器;

//累加器
function add(){
    var count = 0;
    function demo(){
        count++;
        console.log(count);
    }
    return demo;
}
var counter = add();
counter();

這也是閉包的第一個功能,實現共有變數;

1.3 可以做快取

閉包的第二個功能是可以用作快取,比如下面這個例子,我們用push把準備用到的東西放進去,當eat呼叫時使用:

//隱式快取應用
function eater(){
    var food = "";
    var obj = {
        eat: function(){
            console.log("I'm eating " + food);
            food = "";
        },
        push: function(myFood){
            food = myFood;
        }
    }
    return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat();

1.4 可以實現封裝,屬性私有化

這個典型的例子是聖盃繼承實現的雅虎的寫法

雅虎寫法
var inherit = (function(){
    var F = function(){};
    return function(Target,Origin){
        F.prototype = Origin.prototype;
        Target.prototype = new F();
        Target.prototype.constructor = Target;
        //超類
        Target.prototype.uber = Origin.prototype;
    }
}())
//理解,return時保留了F變數,閉包私有化變數

1.5 模組化開發,防止汙染全域性變數

利用閉包變數私有化,避免名稱空間的問題
var name = "heh";
var init = (function(){
   var name = "zhangsan";
   function callName(){
       console.log(name);
   }
   return function (){
       callName();
   }
}())
init();

1.6 閉包的危害

以上四點,其實都是閉包的好處,善加利用是能夠幫助到我們的,所以大家不要先入為主覺得閉包是不好的,閉包其實是我們解決很多問題的一種思路,但當然,閉包確實有它危害的方面:

閉包會導致原有作用域鏈不釋放,造成記憶體洩漏,即佔用導致剩下記憶體變少

1.如何清除閉包:
閉包函式=null;
如第一個例子:a = null;

否則閉包會一直佔用記憶體,直到瀏覽器程式結束。

2.閉包會在父函式外部,改變父函式內部變數的值。

如1.3的例子,我們修改了內部的food值,所以,對於閉包,一定要小心使用。

3.還有一個最常見的情況是for迴圈中的閉包:

我們寫一個ul列表,當點選時輸出對應的i;

clipboard.png

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <script>
        var items = document.getElementsByTagName('li');
        var len = items.length;

        for (var i = 0; i < len; i++) {
            items[i].onclick = function () {
                console.log(i);
            }
        }
    </script>
</body>

這和我們之前事件委託的例子很像,但是這裡我們輸出的不是對應的this物件,而是函式所在作用域的i值,可以看到,我們輸出的都是4,而我們的i應該是從0到1、2、3,加到4的時候已經不滿足條件了,不會進入迴圈。

這到底是怎麼回事呢?

這同樣形成了一個閉包,內部的函式console.log(i)被儲存到了外部的items[i].onclick()之中,所以我們有一個外部的AO,裡面儲存了一個i,但是這個i是for迴圈執行完之後的i,當我們執行點選函式時,始終用到的就是這個i,但這明顯和我們要的不一樣,我們希望每一個執行點選時輸出的都是for迴圈時對應的那個i;

這時候的閉包,是存在一定問題的,利用立即執行函式可以解決這個問題。

2、立即執行函式

立即執行函式,顧名思義,立即會執行的函式(Immediately-Invoked Function Expression),即當js讀取到該函式,會立即執行。
我們1.4、1.5對應的例子中就用到了這個方法。
用法如下:

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>
    <script>
        var items = document.getElementsByTagName('li');
        var len = items.length;

        for (var i = 0; i < len; i++) {
            items[i].onclick = (function () {
                var index = i;
                return function () {
                    console.log(index);
                }
            }())
        }
    </script>
</body>

每次到了立即執行函式時,都會把當前的i賦值給index儲存起來,並返回帶有這個值的函式。

2.1 立即執行函式的寫法

官方的兩種寫法
(function (){}());//w3c建議第一種
(function (){})();

2.2 立即執行函式用於初始化

<!--針對初始化功能的函式-->
var num = (
    function (b) {
        var a = 123;
        console.log(a,b);
        d = a + b;
        return d;
    }(2)
)
<!--返回值賦值給num-->
<!--執行完就被釋放-->

2.3 常見寫法的注意事項

//1.只有表示式才能被執行符號執行,會忽略表示式的名字
//2.我們正常函式執行的寫法如下
function test(){
};
test();
// 但是直接在函式宣告後接執行符號,是不可以的,會報語法錯誤
function test(){
}();

//3.凡是能變成表示式就能被執行
var test = function () {
}();
// 執行一次後被永久銷燬
// 表示式部分 = function(){
// }()

//4.最先識別哪個括號
(--這種寫法最外面先
function test(){}()
--);

(--這種寫法最前面先
function test(){}
--)
();
//可以沒有test名稱

//5.當有引數時,不報錯,但也不執行
function test(a,b,c,d){
    console.log(a+b+c+d);
}(1,2,3,4);

實際分成了兩個部分
function test(a,b,c,d){
    console.log(a+b+c+d);}
和
(1,2,3,4);//4,輸出個數

2.4 作用

透過定義一個匿名函式,建立了一個新的函式作用域,相當於建立了一個“私有”的名稱空間,該名稱空間的變數和方法,不會破壞汙染全域性的名稱空間。此時若是想訪問外部物件,將外部物件以引數形式傳進去即可。

3、let

還是上面那個問題,我們看看下面的程式碼

        // let
        for (let i = 0; i < len; i++) {
            items[i].onclick = function () {
                console.log(i);
            }
        }

和我們第一個版本一樣,只是把var宣告的i換成了let的宣告方式,但是結果已經沒有任何問題了。
為什麼僅僅改動了這一點,就解決了我們之前的問題呢?
其實這個問題的本質原因,還是var帶來的作用域的問題,接下來,我們來看一看,let,到底是什麼?

3.1 什麼是let

let語句,宣告一個塊範圍變數。
let是ES6中新增關鍵字。它的作用類似於var,用來宣告變數,用法也類似,但是let是存在塊級作用域的。
let variable1 = value1;

3.2 特性

1.使用 let 語句宣告一個變數,該變數的範圍限於宣告它的塊中。不能在外部訪問該變數,可以在宣告變數時為變數賦值,也可以稍後在指令碼中給變數賦值。

var  l = 10;
{//注意這裡僅僅是一個塊級作用域
    let l = 2;
    console.log(l);// 這裡 l = 2.
    let m = 4;
    console.log(m);// 這裡 m = 4.
}
console.log(l);// 這裡 l = 10.

console.log(m);//報錯

clipboard.png

相反,對於var,最小級別是函式作用域的。
所以在for迴圈中,var宣告的i是所在函式的作用域,而let則是for迴圈及迴圈體內的作用域,所以裡面的語句可以訪問到對應的i。
2.使用 let 宣告的變數,在宣告前無法使用,否則將會導致錯誤。(不存在變數提升了)

console.log(index);
let index;

clipboard.png

3.如果未在 let 語句中初始化您的變數,則將自動為其分配 JavaScript 值 undefined。

let index;
console.log(index);

clipboard.png

3.3 解讀for迴圈中的let

        for (let i = 0; i < len; i++) {
            items[i].onclick = function () {
                console.log(i);
            }
        }

對於var來說,形成的閉包始終獲取到的都是迴圈完成後被改變的最終的i
而對於let,每一個i都是獨立在當前塊級作用域的,當前的i只在本輪迴圈有效,所以每一次迴圈的i其實都是一個新的變數。而JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。

其實,所謂的for迴圈帶來的閉包問題,其實就是變數作用域的問題,解決方式很多種,基本上可以用立即執行函式和let變數宣告來解決,其次,具體情具體分析。

推薦閱讀

http://web.jobbole.com/82520/
https://www.sogou.com/link?ur...
http://hao.jser.com/archive/5...
https://msdn.microsoft.com/li...
http://www.jb51.net/article/2...
https://segmentfault.com/a/11...

相關文章