深入理解javascript閉包

網(wǎng)上關(guān)于閉包的文章很多姜贡,各種專(zhuān)業(yè)文獻(xiàn)上的“閉包”定義非常抽象功舀,很難看懂。官方對(duì)閉包的解釋是:一個(gè)擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式毫别,還是看不懂娃弓?
不著急,引用《JavaScript權(quán)威指南》英文原版對(duì)閉包的定義如下:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

個(gè)人翻譯為:閉包指的是一個(gè)函數(shù)和該函數(shù)能訪問(wèn)和引用的所有變量對(duì)象(也叫作用域?qū)ο螅┑募?/strong>岛宦。
我覺(jué)的也可以這么理解:一個(gè)閉包就是當(dāng)一個(gè)函數(shù)被其包含作用域外部引用時(shí)台丛,一個(gè)沒(méi)有釋放資源的棧區(qū),棧區(qū)包含該函數(shù)本身和函數(shù)所引用的作用域鏈上的變量對(duì)象砾肺。
所以挽霉,在Javascript中防嗡,從不同角度看有兩種形式:
1)從理論角度看,所有的函數(shù)都是閉包侠坎,包括全局函數(shù)蚁趁,因?yàn)樗麄兌加凶约旱淖兞繉?duì)象和作用域鏈,訪問(wèn)全局變量就相當(dāng)于訪問(wèn)最外層的作用域?qū)ο?br> 2)從實(shí)踐角度看硅蹦,閉包函數(shù)一般有以下特點(diǎn)的:創(chuàng)建它們的上下文(比如父函數(shù)執(zhí)行環(huán)境)已經(jīng)銷(xiāo)毀荣德,它們?nèi)匀淮嬖诓⒈灰茫疫€引用了其作用域鏈里其他作用域?qū)ο蟮淖兞俊?br> 最簡(jiǎn)單的就像這樣:

function F_func() {
  var num = 42;
  var S_func = functio() {
    return ++num;
  }
  return S_func;
}
var add = F_func();

當(dāng)一個(gè)或多個(gè)內(nèi)部函數(shù)被包含函數(shù)返回童芹,而內(nèi)部函數(shù)引用了其他作用域的變量涮瞻,當(dāng)內(nèi)部函數(shù)在包含函數(shù)之外被調(diào)用,我們就可以稱(chēng)它們構(gòu)成了閉包假褪。

一署咽、閉包的原理

1、外部函數(shù)在調(diào)用時(shí)會(huì)創(chuàng)建自身的執(zhí)行環(huán)境生音、變量對(duì)象和作用域鏈宁否,調(diào)用結(jié)束后這些回收,但是函數(shù)的變量對(duì)象若有變量被閉包函數(shù)調(diào)用缀遍,則基于Js的垃圾回收機(jī)制慕匠,函數(shù)調(diào)用結(jié)束后其執(zhí)行環(huán)境、作用域鏈將銷(xiāo)毀域醇,變量對(duì)象卻不會(huì)立即回收台谊,而是在閉包函數(shù)的調(diào)用鏈里;
2譬挚、每調(diào)用一次外部函數(shù)就會(huì)創(chuàng)建自身的執(zhí)行環(huán)境锅铅、變量對(duì)象等操作,便產(chǎn)生一個(gè)新的閉包减宣,以前的閉包依舊存在且互不影響盐须;
3、同一個(gè)閉包會(huì)保留上一次的狀態(tài)漆腌,當(dāng)它被再次調(diào)用時(shí)會(huì)在同一作用域鏈上進(jìn)行贼邓;
還是用上面的例子說(shuō)明

var name = 'scope';
function F_func() {
  var num = 42;
  function S_func() {
    var t = 1;
    num += t;
    return num;
  }
  return S_func;
}
var a = F_func();
a(); // 43
a(); // 44
var b = F_func();  // 重新調(diào)用F_func(),產(chǎn)生新的閉包
b(); // 43  
a = null;   // 通知回收
b(); // 44 

大概的執(zhí)行過(guò)程(其中作用鏈以指針關(guān)聯(lián),vo-指代變量對(duì)象):

  • 作用域鏈的各個(gè)變量對(duì)象以指針關(guān)聯(lián)
  • vo-指代變量對(duì)象

1)進(jìn)入全局上下文闷尿,創(chuàng)建全局執(zhí)行環(huán)境塑径、全局變量對(duì)象和作用域鏈,入棧

   //對(duì)應(yīng)作用域鏈
   global ---> global-vo {
                 name: 'scope',
                 F_func: Function,
                 a: undefined,
                 b: undefined
               }

2)第一次執(zhí)行F_func函數(shù)悠砚,創(chuàng)建F_func執(zhí)行環(huán)境晓勇、變量對(duì)象和作用域鏈堂飞,入棧
3)執(zhí)行F_func函數(shù)灌旧,初始化上下文绑咱、變量對(duì)象和內(nèi)部函數(shù)的作用域鏈

   //對(duì)應(yīng)作用域鏈
   F_func ----- F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                  |                  b: undefined
                  |               }
                  |          
   S_func ----- S_func-vo { 
                   t: 1      
                } 

4)F_func函數(shù)執(zhí)行完畢枢泰,變量a引用返回的內(nèi)部函數(shù)S_func描融,F(xiàn)_func的執(zhí)行環(huán)境和作用域鏈出棧并銷(xiāo)毀

   //對(duì)應(yīng)作用域鏈
                F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                   |                 b: undefined
                   |               }
                   |             
   a=S_func ----- S_func-vo { 
                   t: 1 
                  }

5)再次調(diào)用a函數(shù)窿克,后續(xù)每次調(diào)用都在這個(gè)作用域鏈里進(jìn)行,將會(huì)繼承變量對(duì)象里變量的變化
6)第二次執(zhí)行F_func函數(shù)毛甲,重新創(chuàng)建F_func執(zhí)行環(huán)境...重復(fù)上述過(guò)程年叮,b也將獲得一個(gè)自己的執(zhí)行環(huán)境玻募、變量對(duì)象和作用域鏈,同a是一樣的

   //對(duì)應(yīng)作用域鏈
                F_func-vo { ----- global-vo {
                   num: 42,          name: 'scope',
                   S_func            F_func,
                }                    a: Function,
                   |                 b: undefined
                   |               }
                   |             
   b=S_func ----- S_func-vo { 
                   t: 1 
                  }

7)最后設(shè)置a為null七咧,解除了a和內(nèi)部函數(shù)的引用跃惫,垃圾回收機(jī)制下一次檢查時(shí)將回收存儲(chǔ)并清除內(nèi)部函數(shù)的作用域鏈和變量對(duì)象
8)b不受影響,依然可以調(diào)用
4艾栋、當(dāng)外部函數(shù)中存在多個(gè)內(nèi)部函數(shù)時(shí),這些函數(shù)構(gòu)成閉包引用的同一份父函數(shù)的變量對(duì)象先较,適合構(gòu)造對(duì)象遥诉;
舉個(gè)例子:

function F_func() {
  var num = 42;
  return {
      fun1: function() { console.log(num); },
      fun2: function() { num++; },
      fun3: function() { num--; } 
  };
}
var obj = F_func();
obj.fun2();
obj.fun2();
obj.fun3();
obj.fun1(); // 43

3個(gè)匿名內(nèi)部函數(shù)引用的都是F_func執(zhí)行完生產(chǎn)的同一份變量對(duì)象

二、閉包的用途

閉包可以用在許多地方矮锈,主要有:

  • 讀取函數(shù)內(nèi)部的變量
  • 讓這些變量的值始終保持在內(nèi)存中。
  • 模擬塊級(jí)作用域變量债朵,構(gòu)造變量臨時(shí)性死區(qū)瀑凝,同時(shí)也能創(chuàng)建私有作用域,避免向全局作用域添加過(guò)多變量谚中,減少與其他開(kāi)發(fā)者產(chǎn)生命名沖突
    示例:
(function() {
    var num= 10;
    var add = function(n) {return n + num; }
    ...
})();
  • Js里變量是函數(shù)級(jí)作用域,函數(shù)便是變量的私有作用域宪塔,函數(shù)外不能直接訪問(wèn),只能通過(guò)調(diào)用域鏈比搭,可以封裝私有屬性和私有方法南誊,并開(kāi)發(fā)共有接口,一般用與面向?qū)ο蟮臉?gòu)造
    示例:
function Person(name) {
    var age = 18;
    getAge = function() {
        return age;
    };
    this.getName = function() {
        return name + getAge;
    };
}
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18

三霉赡、注意事項(xiàng)

1)由閉包會(huì)使得函數(shù)中的變量都被保存在內(nèi)存中幔托,內(nèi)存消耗很大,所以不能濫用閉包迫肖,否則會(huì)造成網(wǎng)頁(yè)的性能問(wèn)題和內(nèi)存泄露攒驰。解決方法是,在退出函數(shù)之前玻粪,將不使用的局部變量全部設(shè)置為null
2)閉包增加的變量查找的作用域鏈的長(zhǎng)度劲室,會(huì)在一定程度上影響查找速度

四、實(shí)例分析

(1)修改前

function buildArr(arr) {
   var result = [];
   for (var i = 0; i < arr.length; i++) {
     var item = 'item' + i;
     result.push(function() {console.log(item + ' ' + arr[i])});
   }
   return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0]();  //  item2 undefined
fnlist[1]();  //  item2 undefined
fnlist[2]();  //  item2 undefined

外部函數(shù)buildArr執(zhí)行完后幾個(gè)fnlist匿名函數(shù)的作用域鏈為:

 fnlist[n] {  -----  buildArr-vo {           -----   global-vo {
 }                       result:[Function],              buildArr: Function,
                         i: 3,                           fnlist: undefined
                         item: item2                 }
                         arr: [1,2,3]
                      } 

(2)修改后

function buildArr(arr) {
   var result = [];
   for (var i = 0; i < arr.length; i++) {
      result.push((function(n) {
          var item = 'item' + n;
          return function() {
            console.log(item + ' ' + arr[n]);
         }
     })(i));
  }
  return result;
}
var fnlist = buildArr([1,2,3]);
fnlist[0]();  //  item0 1
fnlist[1]();  //  item1 2
fnlist[2]();  //  item2 3

外部函數(shù)buildArr執(zhí)行完后幾個(gè)fnlist匿名函數(shù)的作用域鏈為:

fnlist[0] {  -----  匿名函數(shù)-vo {  -----  buildArr-vo {          ----- global-vo {
}                       n: 0                result: [Function],          buildArr: Function,
                        item: item0         i: 3,                        fnlist: undefined
                     }                      arr: [1,2,3]              }
                                          } 
                                              |
fnlist[1] {  -----  匿名函數(shù)-vo { -------------- 
}                        n: 1                 |
                         item: item1          |
                     }                        |
fnlist[2] {  -----  匿名函數(shù)-vo {  ------------
}                       n: 2                
                        item: item2         
                     }                          

五、實(shí)現(xiàn)閉包的方法

閉包主要依靠外部和內(nèi)部函數(shù)來(lái)構(gòu)造喉磁,而函數(shù)可以又有多種使用方式,比如:一般函數(shù)涝焙、對(duì)象方法等孕暇,所以可以很多實(shí)現(xiàn)閉包的方法赤兴,只要滿(mǎn)足閉包的特點(diǎn)即可隧哮。
(1)作為普通函數(shù)使用

1.1 最簡(jiǎn)單閉包:
var myFunc = (function() {
    var num = 10;
    return function() {
      return num++;
    };
})();

1.2 返回對(duì)象或數(shù)組:也可叫模塊模式或增強(qiáng)單例對(duì)象
var myArr = (function() {
    var num = 10;
    function add() {
      return num++;
    };
    return [add];
})();
var myObj = (function() {
    var num = 10;
    function add() {
      return num++;
    };
    return {
        add1: add,
        getNum: function() {return num;} // 公共接口
    };
})();

1.3 多級(jí)作用域鏈
var myObj = (function() {
    var num = 10;
    var arr = [];
    for (var i = 0;i < num;i++) {
        arr.push((function(n) {
            return function() { return n + 1;};
        })(i));
    }
    return {
        funs: arr
    };
})();

1.4 閉包函數(shù)以其他方式被引用
var $dom = document.querySelector("#id")
function() {
    var num = 10;
    $dom.onclick = function() {
        console.log(num);
    };
}
function() {
    var num = 10;
    setInterval(function() {
      console.log(num);
    },2000);
}
function() {
    var num = 10;
    clickMe = function() { 
        console.log(num);
    };
}

(2)作為對(duì)象屬性方法使用

var circle = {  
   "PI":3.14159,  
   "area": function(r) {  
                 return this.PI * r * r;  
            }                
};  
console.log(circle.area(2));

(3)作為構(gòu)造函數(shù)和面向?qū)ο?/p>

廣泛用于面向?qū)ο缶幊谭绞街?/p>

3.1 實(shí)例方法
function Person(name) {
    var age = 18;
    getAge = function() {
        return age;
    };
    this.getName = function() {
        return name + getAge;
    };
}
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18

3.2 靜態(tài)方法和變量近迁,靜態(tài)共享
(function() {
    var name = 'lilei';
    var age = 18;
    Person = function(name) {
        name = name;
    };
    Person.prototype.getName = function() {
       return name + age;
    };
})();
var lucy = new Person('Lucy');
lucy.getName();  // Lucy18
var lilei = new Person('Lilei');
lucy.getName();   // Lilei18
lilei.getName();  // Lilei18

3.3 原始對(duì)象
var Circle = new Object();  
Circle.PI = 3.14159;  
Circle.Area = function( r ) {  
       return this.PI * r * r;  
}  
alert( Circle.Area( 1.0 ) );

3.3 工廠模式生產(chǎn)對(duì)象
var Circle = function() {  
   var obj = new Object();  
   obj.PI = 3.14159;  

   obj.area = function( r ) {  
       return this.PI * r * r;  
   }  
   return obj;  
}          
var c = new Circle();  
alert( c.area( 1.0 ) );

3.3 原型模式生產(chǎn)對(duì)象
function Circle(r) {  
      this.r = r;  
}  
Circle.PI = 3.14159;  
Circle.prototype.area = function() {  
  return Circle.PI * this.r * this.r;  
}  
var c = new Circle(1.0);     
alert(c.area());

其他...
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鉴竭,一起剝皮案震驚了整個(gè)濱河市岸浑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌璧眠,老刑警劉巖读虏,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盖桥,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡揩徊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)熄赡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)齿税,“玉大人,你說(shuō)我怎么就攤上這事乌助∧爸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵赏参,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我把篓,道長(zhǎng)韧掩,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任坊谁,我火速辦了婚禮滑臊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雇卷。我一直安慰自己,他們只是感情好小染,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布贮折。 她就那樣靜靜地躺著,像睡著了一般岛都。 火紅的嫁衣襯著肌膚如雪振峻。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,541評(píng)論 1 305
  • 那天烫堤,我揣著相機(jī)與錄音凤价,去河邊找鬼。 笑死利诺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的立倍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼变擒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼寝志!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起材部,我...
    開(kāi)封第一講書(shū)人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤败富,失蹤者是張志新(化名)和其女友劉穎摩窃,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體猾愿,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒂秘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了姻僧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赌莺,死狀恐怖松嘶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翠订,我是刑警寧澤,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布官撼,位于F島的核電站似谁,受9級(jí)特大地震影響燥狰,放射性物質(zhì)發(fā)生泄漏斜筐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一目代、第九天 我趴在偏房一處隱蔽的房頂上張望嗤练。 院中可真熱鬧,春花似錦霜大、人聲如沸革答。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)溪食。三九已至囊卜,卻和暖如春错沃,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笑窜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工登疗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人断傲。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓智政,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親垦垂。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容