JS閉包

一些基礎概念

變量的生命周期

  • 全局作用域下的變量,定義的時候開始存在,頁面關閉或者刷新就消失。
  • 函數內部的變量,在你寫的時候是不存在的,一般來說,只有調用這個函數的時候,這個變量才會被聲明。那么這個函數內部的a什么時候消失呢,當a所在的作用域不見的時候a就消失。
function fn () {
    var a = 1;
    // return undefined; 所有函數不寫return時,默認有這一句
}
// 瀏覽器執行到這一行,此時a不存在

fn();//這個時候a就誕生了
//fn()執行完了之后,a就消失了
fn();//這時候會產生一個新的a,并不是原來的a
//fn() 執行完成后,新的a又消失了
  • 如果這個變量被引用著,就不能回收
function fn () {
    var a = 1;
    var b = 2;
    window.xxx = a;
    // return undefined; 所有函數不寫return時,默認有這一句
}
// 瀏覽器執行到這一行,此時a不存在

fn();//這個時候a就誕生了
//此時fn()執行完,b消失,a還存在,因為a被引用了。
console.log(window.xxx); //1
//等window消失a也就消失了
//其實可以認為函數執行完成后,a這個變量名是消失了的,只是這段內存沒消失,還在繼續被引用,等window消失的時候再釋放這段內存

var作用域

  • 就近原則

    var a;
    function fn1 () {
        var a;
        function fn2 () {
            var a;
            a = 1; //給f2的a賦值
        }
    }
    
    function f2() {};
    function f1 () {
        function f2 () {}
        f2(); //調用的是f1內部的f2
    };
  • 詞法作用域
    var a;
    function fn1 () {
        var a;
        function fn2 () {
            var a;
            a = 1; //給f2的a賦值
        }
    }

不需要執行代碼,分析語句的詞法就能判斷作用域叫詞法作用域

  • 同名的不同變量

立即執行函數

  • 如果想得到一個獨立的作用域,必須要聲明一個函數
  • 如果想運行函數內部的代碼,必須要執行函數
    立即執行函數即創建一個匿名函數作用域并且立即執行。前面可以是! + - 等符號。
  • 關于有沒有var聲明的區別:

var a = 100;

!function() {
    var a = 1;
    !function() {
        a = 2; // 沒有用var聲明,所以不是當前作用域的a,而是上一級的作用域的a,所以改變了上一級a的值
        console.log(a);//2
    }();
    console.log(a);//2   a的值被改變 輸出2
}();
console.log(a);//100
  • 立即執行函數的傳參:
    var a = 1;
    !function(a) { //這里的a是個形參
        console.log(a); //外面傳入一個a,里面的作用域輸出2
    }(2);
    console.log(a); //全局作用域下的a還是1


    //上面的形參a其實相當于下面這么寫
    !function(){
        var a = arguments[0];
        console.log(a);//外面傳入一個a,里面的作用域輸出2
    }(2);

傳參例子2


var a = 100;

!function(a){
    console.log(a);
}(a);
//這里的兩個a,傳入的a是外部傳入的值,全局變量a,而匿名函數的a僅僅是個形參  等于不寫參數,里面一個var a = arguments[0];
//只是在執行函數的時候恰好把傳入參數的a賦值給了內部的a

變量(聲明)提升

  • 瀏覽器在執行代碼之前,會把所有聲明提升到作用域的頂部
var a = 100;
var a = function () {};
function a() {};
console.log(a); //function
//等價于
var a;
var a;
function a () {};
a = 100;
a = function() {};
console.log(a); // 所以是function
  • 函數內部的變量提升
//函數內部的聲明提升,不會提升到全局或者外部的作用域的,就只會提升到當前作用域頂部
function f1() {
    // var a = 100; 變量提升等價于
    var a;
    a = 100;
}
f1();
//另外,上述代碼和下面的是一樣的
function f1() {
    a = 100;
    var a;
}
//只有在當前作用域中有var,就不會跑出當前作用域
//如果是
var a = 1;
function f1 () {
    a = 100;
//    后面語句里沒有用var聲明a,那么這個a就不是當前f1下的a。會在f1的上級作用域中尋找a,并且改變a的值
}
f1();
console.log(a); //100
  • 帶有迷惑性的變量提升
    先進行提升,在考慮代碼
var a = 100;  // 第一個a
function f1 () {
    var b =2;
    if (b === 1) {
        var a; //第二個a
    }
    a = 99;
}
//上述例子中,f1內部寫的a = 99指的是誰? 
// 進行變量提升,首先明確JS沒有塊級作用域,所以什么if for都不是新的作用域,變量提升的時候也不用考慮if的判斷條件
//總而言之,先進行提升,在考慮代碼,即f1內部等價于
var a = 100;
function f1 () {
    var b;
    var a;
    b = 2;
    if(b === 1){
        
    }
    a = 99;
}
//提升之后自然發現指的是f1內部的a

閉包

var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i);
    }
}

上述例子分析:

var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i);
    }
}
console.log(i);

//進行變量提升
var items;
var i;
//這里提升了i,整個作用域中只有這一個i,所以循環到6之后,所有的i的值都為6
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    items[i].onclick = function () {
        console.log(i); //C
    }
}
console.log(i); // D
// 變量提升完成后 D是一定比C先執行的
// 當D執行時 i已經循環為6,所以后面C輸出的都是6


//所以如果要想使得每次輸出的值不一樣,就得使得每次的i不是同一個i
//要取得不同的i,就要創建新的作用域,那么我們來創建新的作用域:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    function temp (i) {//創建一個新的函數作用域,在這個作用域中傳入每次循環得到的i的值,就能得到不一樣的i了
    //    還要注意前面提到的問題,temp(i);中的i和函數的形參i不是同一個東西,具體見立即執行函數的傳參
        items[i].onclick = function () {
            console.log(i); //接收到每次的i值然后賦給i,這樣每次循環的i的值就不一樣了
        }
    }
    temp(i); // 傳入每次循環的i值

}
//那么前面提到的立即執行函數可以簡化代碼,不用寫temp(),直接寫個立即執行函數并且傳入i就好了
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    !function(i) {  //寫成立即執行函數簡化代碼
        items[i].onclick = function () {
            console.log(i); //接收到每次的i值然后賦給i,這樣每次循環的i的值就不一樣了
        }
    }(i);

}

//上述代碼就等價于下面的,temp函數時一個返回函數的函數,創造一個函數用于綁定事件
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    function temp(i) {  // 這里temp(i)其實也是要提升的,但是只要知道內部的i跟外部i不一樣就好了
        return function () {
            console.log(i);
        }
    }

    var fn = temp(i); //傳入每次的i的值  fn必須為一個函數才能賦給onclick事件,所以temp的返回值必須也是一個函數

    items[i].onclick = fn;
}


// 有了返回值為一個函數的思想,我們可以直接給onclick賦給一個返回函數的函數,這樣創建了一個新的作用域保存每次循環的i值,如下:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
    items[i].onclick = function(i) {
        //函數內部作用域的i和外部的i不同
        return function (){
            console.log(i);
        }
    }(i); //傳入每次循環的i值

}

總結一下思路:

  • 要得到每次循環的i值,就必須得創建新的作用域。因為全局作用域當中只有一個i,是不會變的,需要一個新的作用域來獲取每次循環的i的值。
  • 我們給onclick綁定的一定是一個函數,所以賦給onclick一個立即執行函數之后,這個立即執行的值,即return的東西一定是一個函數。這就是為什么一定要return一個函數。

通過作用域鏈理解閉包

第一個例子:

    //通過作用域鏈理解閉包


    //下例中定義一個數組,遍歷數組給每一項賦給一個函數,最后輸出的 fnArr[1]的執行結果()   發現結果都是2,不管是fnArr[1]還是fnArr[0]
    // var fnArr = [];
    // for (var i = 0; i < 2; i ++) {
    //     fnArr[i] = function() {
    //         return i;
    //     }
    // }
    // console.log( fnArr[0]() ); //2
    // console.log( fnArr[1]() ); //2
    //分析: 當我們寫一段函數體或者函數名的時候,就是一段代碼,一個指針或者一個地址,只有后面加了括號(),它才會真正的去執行
    //沒有執行的話,就相當于沒有任何的作用,就是一段代碼。
    //所以在上面的for循環中,賦給fnArr[i]的時候沒有執行,當循環結束時,i的值為2,此時調用fnArr[1]()或者fnArr[0]并且輸出,
    // 此時函數才會真正的執行,但是這個是函數里面沒有i,只會向外部的全局作用域中尋找i,即for循環中的i,為2,所以不管fnArr[0]還是fnArr[1]都輸出2


    //針對上面的例子,我們提出了對代碼的改裝要求,要求fnArr[0]就是輸出0,fnArr[1]就是輸出1。要做到這個效果,就要用到閉包
    // 第一種改裝方法:
    // var fnArr = [];
    // for (var i = 0;i < 2; i ++) {
    //     (function(i){
    //         fnArr[i] = function () {
    //             return i;
    //         }
    //     })(i);
    // }
    // console.log( fnArr[0]() ); //0
    // console.log( fnArr[1]() ); //1
//    針對第一種改裝方法,就等價于下面這么寫,
//    第一種改裝方法的改寫1,去掉for循環:
//     var fnArr = [];
//     (function(i){
//         fnArr[i] = function () {
//             return i;
//         }
//     })(0);
//     (function(i){
//         fnArr[i] = function () {
//             return i;
//         }
//     })(1);
//     console.log( fnArr[0]() );
//     console.log( fnArr[1]() );
//    這種寫法還有立即執行函數,不好理解,接著改寫2,去掉立即執行函數,就等價于:
//    第一種改裝方法的改寫2:
    var fnArr = [];
    function fn1 (i) {
        fnArr[i] = function fn11() {
            return i;
        }
    }
    function fn2 (i) {
        fnArr[i] = function fn22 () {
            return i;
        }
    }
    fn1(0);
    fn2(1);

    console.log( fnArr[0]() );
    console.log( fnArr[1]() );

//    那么通過作用域鏈來分析改寫2:
    globalContext = {
        AO : {
        //    活動對象,變量提升
            fnArr: undefined,
            fn1: function,
            fn2: function,
        }
    }
    fn1.[[scope]] = globalContext.AO;
    fn2.[[scope]] = globalContext.AO;

    fn1Context = {
        AO: {
            i: 0,
            fn11: function,
        },
        scope: fn1.[[scope]]
    }
    fn11.[[scope]] = fn1Context.AO;
    //運行到fn1的時候,沒有自身的AO里面沒有fnArr,所以就要到scope里面去找,fn1.[[scope]]是globalContext的AO,找到了fnArr
    //此時globalContext的AO的fnArr的值就由undefined變為[fn11],然后什么都沒發生,退出來進入fn2
    fn2Context = {
        AO : {
            i: 1,
            fn22: function,
        },
        scope: fn2.[[scope]],
    }
    fn22.[[scope]] = fn2Context.AO;
    //此時進入fn2,尋找fnArr和上面一樣,過程結束后,globalContext的AO里面的fnArr的值為[fn11,fn22];
    //現在    console.log( fnArr[0]() ); 要輸出fnArr[0]() ,即在globalContext中找到AO里面的fnArr[0]并且執行,此時進入fn11的context
    fn11Context = {
        AO: {
        //    AO是空的
        },
        scope: fn11.[[scope]],
    }
    // fn11沒有AO,所以只能通過scope來尋找i,fn11.[[scope]]就是fn1Context.AO, 所以找到了i,i為0,
    // 一樣的,當你輸出fnArr[i]的時候,結果是調用fn22,然后找到fn2中的AO的i,為1
    fn22Context = {
        AO: {
            //    AO是空的
        },
        scope: fn22.[[scope]],
    }
    //那么對比我們一開始沒有達成效果的例子,那個例子是因為在當前函數中找不到i,所以直接去全局中找到了i
    //我們現在改寫了之后,在函數和全局之間又加了一層包裝,在scope的中途就能獲取到i,所以能夠達成效果,這就是閉包

第二個例子:

    //
    function fn () {
        var s  = 1;
        function sum () {
            ++s;
            console.log(s);
        }
        return sum;
    }
    var mySum = fn();
    mySum(); //2
    mySum(); //3
    mySum(); //4
    var mySum2 = fn();
    mySum2(); //2
    mySum2(); //3
    //上述寫法等價于
    // function fn () {
    //     var s = 1;
    //     return function () {
    //         ++s;
    //         console.log(s);
    //     }
    // }
    //同樣的,使用作用域鏈的偽代碼來分析:
    globalContext = {
        AO : {
            fn: function,
            mySum: undefined,
            mySum2: undefined,
        }
    }
    fn.[[scope]] = globalContext.AO;
    //mySum = fn()時,進入fn的執行上下文
    fnContext = {
        AO: {
            s: 1,
            sum: function,
        }
        scope: fn.[[scope]]
    }
    sum.[[scope]] = fnContext.AO;
    //執行完成之后退出fn,此時globalContext的AO里面的mySum的值由undefined變為fn(),即sum,調用mySum(),即執行sum,此時進入sum的context
    sumContext = {
        AO : {
        //    為空
        },
        scope: sum.[[scope]],
    }
//    sum沒有AO,找不到s,所以到scope里面,即fn的AO里面找到s,此時s的值由1變為2.
//    再次執行mySum(),此時sum還是沒有s,繼續到scope中去找,即找到了fnContext的AO的s,此時s為2,所以由2變為3

//    然后執行到了mySum2 = fn(),此時,會初始化一個執行上下文,此時的s為1
    fnContext = {
        AO: {
            s: 1,
            sum: function,
        }
        scope: fn.[[scope]],
    }
    sum.[[scope]] = fnContext.AO;

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容