談js中的做用域鏈和閉包

什麼是做用域

在編程語言中,做用域控制着變量與參數的可見性及生命週期,它能減小名稱衝突,並且提供了自動內存管理(javascript 語言精粹)javascript

靜態做用域

再者,js不像其餘的編程語言同樣,擁有着塊級做用域,就像下面一段代碼。前端

function afunction(){
    var a = 'sf';
    console.log(b);
    console.log(c);
    var b = function(){
        console.log('這是b中的內容');
    }
    function c(){
        console.log('這是c中的內容');
    }
    (function d(){
        console.log('這是d中的內容');
    })()
}

實用var聲明的變量和函數聲明將會進行聲明提早afunction函數的執行環境中,故上述代碼至關於如下的代碼,在一個變量聲明提早的時候,其值爲undefined,而函數聲明則是將函數體做爲值。java

function afunction(){
    var a;
    var b;
    function c(){
        console.log('這是c中的內容');
    }
    a = 'sf';
    console.log(b);
    console.log(c);
    b = function(){
        console.log('這是b中的內容');
    }
    (function d(){
        console.log('這是d中的內容');
    })()
}

全局做用域與局部做用域

將上述的代碼稍做改動以下編程

var outer = 'outer';
function afunction(){
    function c(){
        console.log('這是c中的內容');
    }
    a = 'sf';
    console.log(outer);
}

咱們在afunction函數的外部定義了outer變量,假設這段代碼運行在瀏覽器上,那麼變量提早的過程當中outer變量被聲明在了window做用域上,也就是瀏覽器中的全局做用域上,而函數中的變量則在函數運行時被聲明在了afunction做用域上,這個就是局部做用域,在這個局部做用域中,outer變量被訪問到了,這種跨做用域的讀取變量的形式就是根據做用域鏈來實現的。瀏覽器

什麼是做用域鏈

在js中,函數也是對象,函數與其餘的對象同樣,擁有能夠訪問的屬性,[[Scope]]就是其中的一個屬性,它指明瞭哪些東西能夠被函數訪問。
考慮下面的函數閉包

function add(a,b){
    var sum = a + b;
    return sum;
}

當函數add建立時候,add的[[Scope]]屬性會指向做用域鏈對象,該對象的初始位置指向全局對象,以下圖所示。編程語言

clipboard.png

var t = add(1,2);

上述語句執行了add函數,對於函數的每一次執行,瀏覽器會建立一個執行環境的內部對象,一個執行環境定義了一個函數執行時的環境。函數的每次執行時對應的執行環境都說惟一的。每個執行環境都有本身的做用域鏈,此對象的局部變量,thisarguments等組成活動對象,插入在做用域鏈對象的最前端,也就是圖中所示的0號位置,當運行結束後,執行環境和活動對象都將銷燬。
函數的執行過程當中,每遇到一個變量,都會從做用域鏈的頂部,也就是0號位置查找該變量,若是查找成功則返回,查找失敗則按照做用域鏈查找下一個位置的對象,該例子中也就是1號位置的全局對象。模塊化

clipboard.png

做用域鏈帶來的性能問題

如上面所討論的那樣,每一次遇到讀取變量的時候,都意味着一次搜索做用域鏈的過程,若是搜索的做用域鏈的層次越多的話,將嚴重影響性能。
因此,當在函數中使用全局變量的時候,所產生的代價是最大的,由於全局對象一直處於做用域鏈的最末位置,讀取局部變量是最快的。
因此,一個提升效率的規則是儘量的使用局部變量。以下面的代碼所示。函數

function demo(){
    var d = document,
        bd = d.body,
        div = d.getElementsByTagName('div');
    d.getElementById('id1').innerHTML = 'aaa';
    //(許多使用document,body和div的操做)
}

上面的代碼首先將全局的document對象保存在了局部變量d中,這樣當下次頻繁的使用document對象時,僅僅須要從局部變量中便可得到。性能

動態做用域

js中實用的是靜態做用域,做用域鏈通常不可改變,可是withtry-catch能夠改變做用域鏈,發生在函數的執行時候

with語句

function withTest(){
    var foo = 'sf';
    var obj = {foo:'abc'};
    with(obj){
        function f(){
            alert(foo);
        }
        (function(){
            alert(foo);
        })();
        f();
    }
}
withTest();

在函數聲明的時候,做用域鏈沒有考慮with的狀況,當函數執行的時候,動態生成with的對象,推入在做用域鏈的首位,這就意味着函數的局部變量存在做用域鏈的第二個位置,訪問的代價提升了,雖然訪問with對象的代價下降了,徹底能夠將with對象保存在局部變量中,故with語句不推薦使用。

try-catch語句

try{
    anErrorFunction();
}catch(e){
    errorHandler(e);
}

因爲catch語句中只有一條語句,將error傳遞給errorHandler函數,因此運行時做用域鏈的改變不會影響性能。

什麼是閉包

閉包是容許函數訪問局部做用域以外的數據。即便外部函數已經退出,外部函數的變量仍能夠被內部函數訪問到。
所以閉包的實現須要三個條件:

  • 內部函數實用了外部函數的變量

  • 外部函數已經退出

  • 內部函數能夠訪問

function a(){
    var x = 0;
    return function(y){
        x = x + y;
        return x;
    }
}
var b = a();
b(1);

clipboard.png

上述代碼在執行的時候,b獲得的是閉包對象的引用,雖然a執行完畢後,可是a的活動對象因爲閉包的存在並無被銷燬,在執行b(1)的時候,仍然訪問到了x變量,並將其加1,若在此執行b(1),則x是2,由於閉包的引用b並無消除。

一個經典的閉包的實例

//ul下面有3個li,實現點擊每一個li,彈出li的序號
for(var i = 0,len = lis.length;i < len; i++){
    lis[i].onclick = function(i){
        return function(){
            alert(i);
        }
    }(i);
}

在這裏,沒有把閉包直接給onclick事件,而是先定義了一個自執行函數,該函數中包含着閉包的函數,i的值被保存在自執行的函數中,當閉包函數執行後,會從自執行函數中查找i,達到「保存」變量的目的。

注:匿名函數中的this指向的是window,故在匿名閉包函數使用父函數的this指針時,須要將其存儲下來,如 var that = this;

閉包的做用

  • 模塊化代碼

  • 私有成員

  • 避免全局變量的污染

  • 但願一個變量長期駐紮在內存中

使用閉包所形成的性能問題

如上面的描述,當執行閉包函數後,父函數所保留下來的活動對象並非在閉包函數的做用域鏈的首位(首位存放的是閉包的活動對象),當頻繁的訪問跨做用域的標識符時候,每次都會形成性能的損失,咱們仍然能夠將經常使用的跨做用域變量存儲在局部變量中,直接訪問該局部變量

實用閉包所形成的內存泄露問題(IE9如下)

IE9及如下的版本使用的是引用計數的內存回收機制,當引用計數爲0的時候將會回收,但有一種循環引用的狀況

window.onload = function(){
    var el = document.getElementById("id");
    el.onclick = function(){
        alert(el.id);
    }
}

這段代碼執行時,將匿名函數對象賦值給elonclick屬性;而後匿名函數內部又引用了el對象,存在循環引用,因此不能被回收;
(javascript 高級程序設計(第三版))
解決方法:

window.onload = function(){
    var el = document.getElementById("id");
    var id = el.id; //解除了循環引用
    el.onclick = function(){
        alert(id); //並無出現循環引用
    }
    el = null; // 將閉包引用的外部活動對象清除
}
相關文章
相關標籤/搜索