21_js的閉包了解嗎衩婚?閉包的常見用法說一下

一、閉包的定義

閉包是指有權訪問另一個函數作用域中的變量的函數 --《JavaScript高級程序設計》

函數對象可以通過作用域關聯起來效斑,函數體內的變量都可以保存在函數作用域內非春,這在計算機科學文獻中稱為“閉包”,所有的javascirpt函數都是閉包 --《Javascript權威指南》

相關概念學習:

  • 作用域
  • 執(zhí)行上下文
  • 執(zhí)行上下文堆棧
  • 變量對象
  • 活動對象
  • 作用域鏈

1、作用域 Scope

作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標識符)
作用域共有兩種主要的工作模型:

  • 詞法作用域:作用域是在編寫代碼的時候確定的
  • 動態(tài)作用域:作用域是在代碼運行的時候確定的

javascript使用的是詞法作用域

2奇昙、執(zhí)行上下文 Execution Contexts

Javascript中代碼的執(zhí)行上下文分為以下三種:

  • 全局級別的代碼 – 這個是默認的代碼運行環(huán)境护侮,一旦代碼被載入,引擎最先進入的就是這個環(huán)境储耐。
  • 函數級別的代碼 – 當執(zhí)行一個函數時羊初,運行函數體中的代碼。
  • Eval的代碼 – 在Eval函數內運行的代碼什湘。

一個執(zhí)行的上下文可以抽象的理解為一個對象长赞。每一個執(zhí)行的上下文都有一系列的屬性(變量對象(variable object),this指針(this value)闽撤,作用域鏈(scope chain) )

Execution Contexts = {
    variable object:變量對象;
    this value: this指針;
    scope chain:作用域鏈;
}

3得哆、執(zhí)行上下文堆棧 Execution Contexts Stack

活動的執(zhí)行上下文在邏輯上組成一個堆棧。堆棧底部永遠都是全局上下文(globalContext)腹尖,而頂部就是當前(活動的)執(zhí)行上下文。

<script>
    function add(num){
        var sum = 5;
        return sum + num;
    }
    var sum = add(4);
</script>

當add函數被調用時伐脖,add函數執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端热幔,此時執(zhí)行上下文堆棧可表示為:

EC Stack = [
  <add> functionContext
  globalContext
];

add函數執(zhí)行完畢后讼庇,其執(zhí)行上下文將會從執(zhí)行上下文堆棧頂端彈出并被銷毀绎巨。全局執(zhí)行上下文只有在瀏覽器關閉時才會從執(zhí)行上下文堆棧中銷毀

4、變量對象 Variable Object

如果變量與執(zhí)行上下文相關蠕啄,那變量自己應該知道它的數據存儲在哪里场勤,并且知道如何訪問。這種機制稱為變量對象(variable object)歼跟。
可以說變量對象是與執(zhí)行上下文相關的數據作用域(scope of data) 和媳。它是與執(zhí)行上下文關聯的特殊對象,用于存儲被定義在執(zhí)行上下文中的變量(variables)哈街、函數聲明(function declarations) 留瞳。
當進入全局上下文時,全局上下文的變量對象可表示為:

VO = {
    add: <reference to function>,
    sum: undefined,
    Math: <...>,
    String: <...>
    ...
    window: global //引用自身
}

5骚秦、活動對象 Activation Object

當函數被調用者激活時她倘,這個特殊的活動對象(activation object) 就被創(chuàng)建了。它包含普通參數(formal parameters) 與特殊參數(arguments)對象(具有索引屬性的參數映射表)作箍∮擦海活動對象在函數上下文中作為變量對象使用。
當add函數被調用時胞得,add函數執(zhí)行上下文被壓入執(zhí)行上下文堆棧的頂端荧止,add函數執(zhí)行上下文中活動對象可表示為

AO = {
    num: 4,
    sum :5,
    arguments:{0:4}
}

6、作用域鏈 Scope Chain

函數上下文的作用域鏈在函數調用時創(chuàng)建的,包含活動對象AO和這個函數內部的[[scope]]屬性罩息。

var x = 10;
function foo() {
  var y = 20;
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
  bar();
}
foo(); 

在這段代碼中我們看到變量"y"在函數"foo"中定義(意味著它在foo上下文的AO中)"z"在函數"bar"中定義嗤详,但是變量"x"并未在"bar"上下文中定義,相應地瓷炮,它也不會添加到"bar"的AO中葱色。乍一看,變量"x"相對于函數"bar"根本就不存在娘香;
函數"bar"如何訪問到變量"x"苍狰?理論上函數應該能訪問一個更高一層上下文的變量對象。實際上它正是這樣烘绽,這種機制是通過函數內部的[[scope]]屬性來實現的淋昭。
[[scope]]是所有父級變量對象的層級鏈,處于當前函數上下文之上安接,在函數創(chuàng)建時存于其中翔忽。

注意: [[scope]]在函數創(chuàng)建時被存儲是靜態(tài)的(不變的),直至函數銷毀盏檐。即:函數可以永不調用歇式,但[[scope]]屬性已經寫入,并存儲在函數對象中胡野。
在這里我們逐步分析下
全局上下文的變量對象是:

globalContext.VO === Global = {
  x: 10
  foo: <reference to function>
};

在"foo"創(chuàng)建時材失,"foo"的[[scope]]屬性是:

foo.[[Scope]] = [
  globalContext.VO
];

在"foo"激活時(進入上下文),"foo"上下文的活動對象是:

fooContext.AO = {
  y: 20,
  bar: <reference to function>
};

"foo"上下文的作用域鏈為:

fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

內部函數"bar"創(chuàng)建時硫豆,其[[scope]]為:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

在"bar"激活時龙巨,"bar"上下文的活動對象為:

barContext.AO = {
  z: 30
};

"bar"上下文的作用域鏈為:

bar.Scope= [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

二熊响、閉包的原理

我們通過一個閉包的例子來分析一下閉包的形成原理

function add(){
    var sum =5;
    var func = function () {
        console.log(sum);
    }
    return func;
}
var addFunc = add();
addFunc(); //5

js執(zhí)行流進入全局執(zhí)行上下文環(huán)境時,全局執(zhí)行上下文可表示為:

globalContext = {
    VO: {
        add: <reference to function>,
        addFunc: undefined
    },
    this: window,
    scope chain: window 
}

當add函數被調用時旨别,add函數執(zhí)行上下文可表示為:

addContext = {
    AO: {
        sum: undefined //代碼進入執(zhí)行階段時此處被賦值為5
        func: undefined //代碼進入執(zhí)行階段時此處被賦值為function (){console.log(sum);}
    },
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}

add函數執(zhí)行完畢后,js執(zhí)行流回到全局上下文環(huán)境中汗茄,將add函數的返回值賦值給addFunc昼榛。

由于addFunc仍保存著func函數的引用,所以add函數執(zhí)行上下文從執(zhí)行上下文堆棧頂端彈出后并未被銷毀而是保存在內存中剔难。

當addFunc()執(zhí)行時胆屿,func函數被調用,此時func函數執(zhí)行上下文可表示為:

funcContext = {
    this: window,
    scope chain: addContext.AO + globalContext.VO 
}

當要訪問變量sum時偶宫,func的活動對象中未能找到非迹,則會沿著作用域鏈查找,由于js遵循詞法作用域纯趋,作用域在函數創(chuàng)建階段就被確定憎兽,在add函數的活動對象中找到sum = 5;

形成閉包的原因:

Javascript允許使用內部函數---即函數定義和函數表達式位于另一個函數的函數體內冷离。而且,這些內部函數可以訪問它們所在的外部函數中聲明的所有局部變量纯命、參數和聲明的其他內部函數西剥。當其中一個這樣的內部函數在包含它們的外部函數之外被調用時,就會形成閉包亿汞。

三瞭空、閉包的用途

閉包可以用在許多地方。它的最大用處有兩個:

  • 讀取函數內部的變量
  • 讓這些變量的值始終保持在內存中

1疗我、保護變量的安全實現JS私有屬性和私有方法

利用閉包可以讀取函數內部的變量咆畏,變量在函數外部不能直接讀取到,從而達到保護變量安全的作用吴裤。因為私有方法在函數內部都能被訪問到旧找,從而實現了私有屬性和方法的共享。
常見的模塊模式就是利用閉包的這種特性建立的

var Counter = (function() {
  //私有屬性
  var privateCounter = 0; 
  //私有方法
  function changeBy(val) { 
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();
console.log(privateCounter); //privateCounter is not defined 
console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1

在jQuery框架的私有方法和變量也是這么設計的

var $ = jQuery = function(){
    return jQuery.fn.init();
}
jQuery.fn = jQuery.prototype = {
    init:function(){
        return this;  //this指向jQuery.prototype
    },
    length: 1,
    size: function(){
        return this.length;
    }
}
console.log($().size()); // 1

2麦牺、將處理結果緩存

var mult = (function(){
    var cache = {};
    var calculate = function(){
        var a = 1;
        for(vari=0,l=arguments.length;i<l;i++){
            a = a*arguments[i];
        }
        return a;    
    };
    return function(){
        var args = Array.prototype.join.call(arguments,',');
        if(args in cache){
            return cache[args];
        }
        return cache[args] = calculate.apply(null,arguments);    
    }
})();

這樣我們在第二次調用的時候拂苹,就會從緩存中讀取到該對象赫粥。

3禁炒、模塊化代碼

var mod = (function(){  //mod是一個模塊脉漏,私有變量是a榴芳,私有函數是func1和func2
    var a = 1;
    function func1(){
        a++;
        alert(a);
    }
    function func2(){
        a++;
        alert(a);
    }
    return {
        b:func1,
        c:func2
    }
})()  //函數表達式自執(zhí)行羞酗,結果是return后面的對象
mod.b(); //2
mod.c(); //3

4殴俱、在循環(huán)中找到索引

<body>
    <ul>
        <li>111111</li>
        <li>111111</li>
        <li>111111</li>
    </ul>
    <script>
        window.onload = function(){
            var aLi = document.getElementsByTagName('li');
            for(var i=0;i<aLi.length;i++){
                aLi[i].onclick = function(){
                    alert(i); //順序點擊三個li养交,分別彈出3 3 3易结。因為點擊的時候for循環(huán)已經執(zhí)行完畢
                }
            }
        }
    </script>
</body>

可使用閉包改為:

<body>
    <ul>
        <li>111111</li>
        <li>111111</li>
        <li>111111</li>
    </ul>
    <script>
        window.onload = function(){
            var aLi = document.getElementsByTagName('li');
            for(var i=0;i<aLi.length;i++){
                (function(i){
                    aLi[i].onclick = function(){
                        alert(i); //0 1 2
                    }
                })(i) //將i作為參數傳遞給內部函數枕荞,i 在for循環(huán)執(zhí)行完畢不會被釋放
            }
        }
    </script>
</body>

或者另一種寫法:

<body>
    <ul>
        <li>111111</li>
        <li>111111</li>
        <li>111111</li>
    </ul>
    <script>
        window.onload = function(){
            var aLi = document.getElementsByTagName('li');
            for(var i=0;i<aLi.length;i++){
                aLi[i].onclick = (function(i){
                    return function(){
                        alert(i);
                    }
                })(i) //循環(huán)的時候,這個函數表達式自執(zhí)行搞动,i也是駐扎在內存當中躏精。
            }
        }
    </script>
</body>

理解了閉包的原理我們發(fā)現閉包的這些用途都是利用了閉包保存了當前函數的活動對象的特點,這樣閉包函數在作用域之外被調用時依然能夠訪問其創(chuàng)建時的作用域

四鹦肿、閉包的缺點

  • 閉包將函數的活動對象維持在內存中矗烛,過度使用閉包會導致內存占用過多,所以在使用完后需要將保存在內存中的活動對象解除引用箩溃;
  • 閉包只能取得外部函數中任何變量的最后一個值瞭吃,在使用循環(huán)且返回的函數中帶有循環(huán)變量時會得到錯誤結果;
  • 當返回的函數為匿名函數時涣旨,注意匿名函數中的this指的是window對象歪架。

五、閉包面試題解

1霹陡、用閉包實現計數器

function addCount() {
  var count = 0;
  return function() {
    count = count + 1;
    console.log(count);
  };
}

addCount() 執(zhí)行的時候, 返回一個函數, 函數是可以創(chuàng)建自己的作用域的, 但是此時返回的這個函數內部需要引用 addCount() 作用域下的變量 count, 因此這個 count 是不能被銷毀的.接下來需要幾個計數器我們就定義幾個變量就可以,并且他們都不會互相影響,每個函數作用域中還會保存 count 變量不被銷毀,進行不斷的累加和蚪。

var fun1 = addCount();
fun1(); //1
fun1(); //2
var fun2 = addCount();
fun2(); //1
fun2(); //2

2止状、for 循環(huán)中打印

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

上邊打印出來的都是 4, 可能部分人會認為打印的是 0,1,2,3

原因:js 執(zhí)行的時候首先會先執(zhí)行主線程,異步相關的會存到異步隊列里,當主線程執(zhí)行完畢開始執(zhí)行異步隊列, 主線程執(zhí)行完畢后,此時 i 的值為 4,所以在執(zhí)行異步隊列的時候,打印出來的都是 4(這里需要大家對 event loop 有所了解(js 的事件循環(huán)機制))

如何修改使其正常打印:采用閉包使其正常打印

//方法一:
for (var i = 0; i < 4; i++) {
  setTimeout(
    (function(i) {
      return function() {
        console.log(i);
      };
    })(i),
    300
  );
}
// 或者
for (var i = 0; i < 4; i++) {
  setTimeout(
    (function() {
      var temp = i;
      return function() {
        console.log(temp);
      };
    })(),
    300
  );
}
// 這個是通過自執(zhí)行函數返回一個函數,然后在調用返回的函數去獲取自執(zhí)行函數內部的變量,此為閉包

//方法二:
for (var i = 0; i < 4; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, 300);
  })(i);
}
// 方法二是通過創(chuàng)建一個自執(zhí)行函數,使變量存在這個自執(zhí)行函數的作用域里

3、真實的獲取多個元素并添加點擊事件

var op = document.querySelectorAll("p");
for (var j = 0; j < op.length; j++) {
  op[j].onclick = function() {
    alert(j);
  };
}
//alert出來的值是一樣的
// 解決辦法一:
for (var j = 0; j < op.length; j++) {
  (function(j) {
    op[j].onclick = function() {
      alert(j);
    };
  })(j);
}

// 解決辦法二:
for (var j = 0; j < op.length; j++) {
  op[j].onclick = (function(j) {
    return function() {
      alert(j);
    };
  })(j);
}

//解決方法三
for (var j = 0; j < op.length; j++) {
  op[j].onclick = (function(j) {
    var temp = j;
    return function() {
      alert(j);
    };
  })(j);
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末攒霹,一起剝皮案震驚了整個濱河市怯疤,隨后出現的幾起案子,更是在濱河造成了極大的恐慌催束,老刑警劉巖集峦,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異泣崩,居然都是意外死亡少梁,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門矫付,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凯沪,“玉大人,你說我怎么就攤上這事买优》谅恚” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵杀赢,是天一觀的道長烘跺。 經常有香客問我,道長脂崔,這世上最難降的妖魔是什么滤淳? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮砌左,結果婚禮上脖咐,老公的妹妹穿的比我還像新娘。我一直安慰自己汇歹,他們只是感情好屁擅,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著产弹,像睡著了一般派歌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上痰哨,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天胶果,我揣著相機與錄音,去河邊找鬼斤斧。 笑死早抠,一個胖子當著我的面吹牛,可吹牛的內容都是我干的折欠。 我是一名探鬼主播贝或,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼吼过,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了咪奖?” 一聲冷哼從身側響起盗忱,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎羊赵,沒想到半個月后趟佃,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡昧捷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年闲昭,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片靡挥。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡序矩,死狀恐怖,靈堂內的尸體忽然破棺而出跋破,到底是詐尸還是另有隱情簸淀,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布毒返,位于F島的核電站租幕,受9級特大地震影響,放射性物質發(fā)生泄漏拧簸。R本人自食惡果不足惜劲绪,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盆赤。 院中可真熱鬧贾富,春花似錦、人聲如沸弟劲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兔乞。三九已至,卻和暖如春凉唐,著一層夾襖步出監(jiān)牢的瞬間庸追,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工台囱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淡溯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓簿训,卻偏偏與公主長得像咱娶,于是被迫代替她去往敵國和親米间。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內容