溫故js系列(14)-閉包&垃圾回收&內存泄露&閉包應用&做用域鏈&再析閉包

前端學習:教程&開發模塊化/規範化/工程化/優化&工具/調試&值得關注的博客/Git&面試-前端資源彙總前端

歡迎提issues斧正:閉包git

JavaScript-閉包

閉包(closure)是一個讓人又愛又恨的something,它能夠實現不少高級功能和應用,同時在理解和應用上有不少難點和須要當心注意的地方。github

閉包的定義

閉包,官方對閉包的解釋是:一個擁有許多變量和綁定了這些變量的環境的表達式(一般是一個函數),於是這些變量也是該表達式的一部分。
簡單來講,閉包就是可以讀取其餘函數內部變量的函數。在Javascript中,只有函數內部的子函數才能讀取函數的局部變量,因此,能夠把閉包理解成:定義在一個函數內部的函數,也就是函數嵌套函數,給函數內部和函數外部搭建起一座橋樑。面試

閉包的特色

  1. 定義在一個函數內部的函數。
  2. 函數內部能夠引用函數外部的參數和變量。
  3. 做爲一個函數變量的一個引用,當函數返回時,其處於激活狀態。
  4. 當一個函數返回時,一個閉包就是一個沒有釋放資源的棧區。函數的參數和變量不會被垃圾回收機制回收。

閉包的造成

Javascript容許使用內部函數,能夠將函數定義和函數表達式放在另外一個函數的函數體內。並且,內部函數能夠訪問它所在的外部函數聲明的局部變量、參數以及聲明的其餘內部函數。當其中一個這樣的內部函數在包含它們的外部函數以外被調用時,就會造成閉包。瀏覽器

function a() {  
    var i = 0;  
    function b() { 
        console.log(i++); 
    }  
    return b; 
}
var c = a(); 
c();

閉包的缺點

1.因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大。因此在閉包不用以後,將不使用的局部變量刪除,使其被回收。在IE中可能致使內存泄露,即沒法回收駐留在內存中的元素,這時候須要手動釋放。安全

function a() {  
    var i = 1;  
    function b() { 
        console.log(i++); 
    }  
    return b; 
}
var c = a(); 
c(); //1
c(); //2
c(); //3   i不被回收
c = null;  //i被回收

2.閉包會在父函數外部,改變父函數內部變量的值。若是你把父函數看成對象使用,把閉包看成它的公用方法,把內部變量看成它的私有屬性,要當心,不要隨便改變父函數內部變量的值。閉包

var Xzavier = { 
    ten:10,  
    addTen: function(num) {  
       return this.ten + num;   //給一個數加10 
   }    
}
 
console.log(Xzavier.addTen(15));  //25
Xzavier.ten = 20; 
console.log(Xzavier.addTen(15));  //35

內存泄露

內存泄漏指一塊被分配的內存既不能使用,又不能回收,直到瀏覽器進程結束。模塊化

出現緣由:函數

1.循環引用:含有DOM對象的循環引用將致使大部分當前主流瀏覽器內存泄露。循環 引用,簡單來講假如a引用了b,b又引用了a,a和b就構成了循環引用。
2.JS閉包:閉包,函數返回了內部函數還能夠繼續訪問外部方法中定義的私有變量。
3.Dom泄露,當原有的DOM被移除時,子結點引用沒有被移除則沒法回收。

JavaScript垃圾回收機制

Javascript中,若是一個對象再也不被引用,那麼這個對象就會被GC(garbage collection)回收。若是兩個對象互相引用,而再也不被第3者所引用,那麼這兩個互相引用的對象也會被回收。垃圾回收不是時時的,由於其開銷比較大,因此垃圾回收器會按照固定的時間間隔週期性的執行。工具

函數a被b引用,b又被a外的c引用,這就是爲何函數a執行後不會被回收的緣由。

垃圾回收的兩個方法:

標記清除法:

1.垃圾回收機制給存儲在內存中的全部變量加上標記,而後去掉環境中的變量以及被環境中變量所引用的變量(閉包)。
2.操做1以後內存中仍存在標記的變量就是要刪除的變量,垃圾回收機制將這些帶有標記的變量回收。

引用計數法:

1.垃圾回收機制給一個變量一個引用次數,當聲明瞭一個變量並將一個引用類型賦值給該變量的時候這個值的引用次數就加1。
2.當該變量的值變成了另一個值,則這個值得引用次數減1。
3.當這個值的引用次數變爲0的時候,說明沒有變量在使用,垃圾回收機制會在運行的時候清理掉引用次數爲0的值佔用的空間。

閉包的應用

1.維護函數內的變量安全,避免全局變量的污染。

函數a中i只有函數b才能訪問,而沒法經過其餘途徑訪問到。

function xzavier(){
    var i = 1;
    i++;
    console.log(i);
}
xzavier();   //2 
console.log(x);   // x is not defined                 
xzavier();   //2

2.維持一個變量不被回收。

因爲閉包,函數a中i的一直存在於內存中,所以每次執行c(),都會給i自加1,且i不被垃圾回收機制回收。

function a() {  
    var i = 1;  
    function b() { 
        console.log(i++); 
    }  
    return b; 
}
var c = a(); 
c();  //1
c();  //2
c();  //3

3.經過第1點的特性設計私有的方法和屬性。

var xzavier = (function(){
    var i = 1;
    var s = 'xzavier';
    function f(){
        i++;
        console.log(i);
    }
    return {
        i:i,
        s:s,             
        f:f
    }
})();
xzavier.s;     //'xzavier'
xzavier.s;     //1
xzavier.f()    //2

4.操做DOM獲取目標元素

方法2即便用了閉包的方法,固然操做DOM仍是有別的方法的,好比事件委託就比較好用。

ul id="test">
    <li>first</li>
    <li>second</li>
    <li>third</li>
</ul>
// 方法一:this方法
var lis = document.getElementById('test').getElementsByTagName('li');
for(var i = 0;i < 3;i++){
    lis[i].index = i;
    lis[i].onclick = function(){
        console.log(this.index);
    };
} 
// 方法二:閉包方法
var lis = document.getElementById('test').getElementsByTagName('li');
for(var i = 0;i < 3;i++){
    lis[i].index = i;
    lis[i].onclick = (function(val){
        return function() {
            console.log(val);
        }
    })(i);
}
// 方法三 事件委託方法
var oUl = document.getElementById('test');
oUl.addEventListener('click',function(e){
    var lis = e.target;
    console.log(lis); 
});

5.封裝模塊

邏輯隨業務複雜而複雜O(∩_∩)O~

var Xzavier = function(){       
    var name = "xzavier";       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}();    

console.log(person.name); //undefined,變量做用域爲函數內部,外部沒法訪問    
console.log(person.getName()); // "xzavier" 
person.setName("xz");    
console.log(person.getName());  //"xz"

6.實現類和繼承

function Xzavier(){       
    var name = "xzavier";       
    return {    
       getName : function(){    
           return name;    
       },    
       setName : function(newName){    
           name = newName;    
       }    
    }    
}

var xz = new Xzavier();  //Xzavier就是一個類,能夠實例化
console.log(xz.getName());  // "xzavier"

這裏是原型繼承,我會在下一篇文章講一講原型繼承。

var X = function(){};
X.prototype = new Xzavier(); 
X.prototype.sports = function(){
    console.log("basketball");
};
var x = new X();
x.setName("xz");
x.sports();  //"basketball"
console.log(x.getName());  //"xz"

JavaScript做用域鏈

JavaScript做用域

做用域就是變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期。
在JavaScript中,變量的做用域有全局做用域和局部做用域兩種。

JavaScript做用域鏈

JavaScript函數對象擁有能夠經過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。
其中一個內部屬性是[[Scope]],該內部屬性包含了函數被建立的做用域中對象的集合。
這個集合被稱爲函數的做用域鏈。

執行上下文

當函數執行時,會建立一個執行上下文(execution context),執行上下文是一個內部對象,定義了函數執行時的環境。
每一個執行上下文都有本身的做用域鏈,用於標識符解析。
當執行上下文被建立時,而它的做用域鏈初始化爲當前運行函數的[[Scope]]包含的對象。

活動對象

這些值按照它們出如今函數中的順序被複制到執行上下文的做用域鏈中。
它們共同組成了一個新的對象,活動對象(activation object)。
該對象包含了函數的全部局部變量、命名參數、參數集合以及this,而後此對象會被推入做用域鏈的前端。
當執行上下文被銷燬,活動對象也隨之銷燬。
活動對象是一個擁有屬性的對象,但它不具備原型並且不能經過JavaScript代碼直接訪問。

查找機制:

1.當函數訪問一個變量時,先搜索自身的活動對象,若是存在則返回,若是不存在將繼續搜索函數父函數的活動對象,依次查找,直到找到爲止。
2.若是函數存在prototype原型對象,則在查找完自身的活動對象後先查找自身的原型對象,再繼續查找。
3.若是整個做用域鏈上都沒法找到,則返回undefined。

在執行上下文的做用域鏈中,標識符所在的位置越深,讀寫速度就會越慢。全局變量老是存在於執行上下文做用域鏈的最末端,所以在標識符解析的時候,查找全局變量是最慢的。

so

在編寫代碼的時候應儘可能少使用全局變量,儘量使用局部變量。
咱們常用局部變量先保存一個屢次使用的須要跨做用取的值再使用。

再析閉包

function a() {  
    var i = 1;  
    function b() { 
        console.log(i++); 
    }  
    return b; 
}
var c = a(); 
c();

1.當定義函數a,js解釋器將函數a的做用域鏈設置爲定義a時a所在的環境。
2.執行函數a的時候,a會進入相應的執行上下文。
3.在建立執行上下文的過程當中,首先會爲a添加一個scope屬性,即a的做用域,其值就爲a的做用域鏈。
4.而後執行上下文會建立一個活動對象。
5.建立完活動對象後,把活動對象添加到a的做用域鏈的最頂端。此時a的做用域鏈包含了兩個對象:a的活動對象和window對象。
6.接着在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
7.最後把全部函數a的形參和內部的函數b的引用也添加到a的活動對象上。    
  在這一步中,完成了函數b的的定義(如同a),函數b的做用域鏈被設置爲b所被定義的環境,即a的做用域。
8.整個函數a從定義到執行的步驟完成。

a返回函數b的引用給c,由於函數b的做用域鏈包含了對函數a的活動對象的引用,也就是說b能夠訪問到a中定義的全部變量和函數。函數b被c引用,函數b又依賴函數a,所以函數a在返回後不會被GC回收,因此造成了閉包。

相關文章
相關標籤/搜索