深刻javascript——做用域和閉包

做用域和做用域鏈是javascript中很是重要的特性,對於他們的理解直接關係到對於整個javascript體系的理解,而閉包又是對做用域的延伸,也是在實際開發中常用的一個特性,實際上,不只僅是javascript,在不少語言中都提供了閉包的特性。javascript

做用域

做用域是一個變量和函數的做用範圍,javascript中函數內聲明的全部變量在函數體內始終是可見的,在javascript中有全局做用域和局部做用域,可是沒有塊級做用域,局部變量的優先級高於全局變量,經過幾個示例來了解下javascript中做用域的那些「潛規則」(這些也是在前端面試中常常問到的問題)。
1. 變量聲明提早
示例1:前端

var scope="global";
function scopeTest(){
    console.log(scope);
    var scope="local"  
}
scopeTest(); //undefined

此處的輸出是undefined,並無報錯,這是由於在前面咱們提到的函數內的聲明在函數體內始終可見,上面的函數等效於:java

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //local

注意,若是忘記var,那麼變量就被聲明爲全局變量了。
2. 沒有塊級做用域
和其餘咱們經常使用的語言不一樣,在Javascript中沒有塊級做用域:面試

function scopeTest() {
    var scope = {};
    if (scope instanceof Object) {
        var j = 1;
        for (var i = 0; i < 10; i++) {
            //console.log(i);
        }
        console.log(i); //輸出10
    }
    console.log(j);//輸出1

}

在javascript中變量的做用範圍是函數級的,即在函數中全部的變量在整個函數中都有定義,這也帶來了一些咱們稍不注意就會碰到的「潛規則」:segmentfault

var scope = "hello";
function scopeTest() {
    console.log(scope);//①
    var scope = "no";
    console.log(scope);//②
}

在①處輸出的值居然是undefined,簡直喪心病狂啊,咱們已經定義了全局變量的值啊,這地方不該該爲hello嗎?其實,上面的代碼等效於:瀏覽器

var scope = "hello";
function scopeTest() {
    var scope;
    console.log(scope);//①
    scope = "no";
    console.log(scope);//②
}

聲明提早、全局變量優先級低於局部變量,根據這兩條規則就不難理解爲何輸出undefined了。閉包

做用域鏈

在javascript中,每一個函數都有本身的執行上下文環境,當代碼在這個環境中執行時,會建立變量對象的做用域鏈,做用域鏈是一個對象列表或對象鏈,它保證了變量對象的有序訪問。
做用域鏈的前端是當前代碼執行環境的變量對象,常被稱之爲「活躍對象」,變量的查找會從第一個鏈的對象開始,若是對象中包含變量屬性,那麼就中止查找,若是沒有就會繼續向上級做用域鏈查找,直到找到全局對象中:函數

做用域鏈的逐級查找,也會影響到程序的性能,變量做用域鏈越長對性能影響越大,這也是咱們儘可能避免使用全局變量的一個主要緣由。性能

閉包

  • 基礎概念

做用域是理解閉包的一個前提,閉包是指在當前做用域內老是能訪問外部做用域中的變量。ui

function createClosure(){
    var name = "jack";
    return {
        setStr:function(){
            name = "rose";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面的示例在函數中返回了兩個閉包,這兩個閉包都維持着對外部做用域的引用,所以無論在哪調用老是可以訪問外部函數中的變量。在一個函數內部定義的函數,會將外部函數的活躍對象添加到本身的做用域鏈中,所以上面實例中經過內部函數可以訪問外部函數的屬性,這也是javascript模擬私有變量的一種方式。
請輸入圖片描述
注意:因爲閉包會額外的附帶函數的做用域(內部匿名函數攜帶外部函數的做用域),所以,閉包會比其它函數多佔用些內存空間,過分的使用可能會致使內存佔用的增長。

  • 閉包中的變量
    在使用閉包時,因爲做用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引發的一個反作用就是若是內部函數在一個循環中,那麼變量的值始終爲最後一個值。
//該實例不太合理,有必定延遲因素,此處主要爲了說明閉包循環中存在的問題
    function timeManage() {
        for (var i = 0; i < 5; i++) {
            setTimeout(function() {
                console.log(i);
            },1000)
        };
    }

上面的程序並無按照咱們預期的輸入1-5的數字,而是5次所有輸出了5。再來看一個示例:

function createClosure(){
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = function(){
            return i;
        }
    }
    return result;
}

調用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。經過以上兩個例子能夠看出閉包在帶有循環的內部函數使用時存在的問題:由於每一個函數的做用域鏈中都保存着對外部函數(timeManage、createClosure)的活躍對象,所以,他們都引用着同一變量i,當外部函數返回時,此時的i值爲5,因此內部的每一個函數i的值也爲5。
那麼如何解決這個問題呢?咱們能夠經過匿名包裹器(匿名自執行函數表達式)來強制返回預期的結果:

function timeManage() {
    for (var i = 0; i < 5; i++) {
        (function(num) {
            setTimeout(function() {
                console.log(num);
            }, 1000);
        })(i);
    }
}

或者在閉包匿名函數中再返回一個匿名函數賦值:

function timeManage() {
    for (var i = 0; i < 10; i++) {
        setTimeout((function(e) {
            return function() {
                console.log(e);
            }
        })(i), 1000)
    }
}
//timeManager();輸出1,2,3,4,5
function createClosure() {
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = function(num) {
            return function() {
                console.log(num);
            }
        }(i);
    }
    return result;
}
//createClosure()[1]()輸出1;createClosure()[2]()輸出2

不管是匿名包裹器仍是經過嵌套匿名函數的方式,原理上都是因爲函數是按值傳遞,所以會將變量i的值複製給實參num,在匿名函數的內部又建立了一個用於返回num的匿名函數,這樣每一個函數都有了一個num的副本,互不影響了。

  • 閉包中的this

在閉包中使用this時要特別注意,稍微不慎可能會引發問題。一般咱們理解this對象是運行時基於函數綁定的,全局函數中this對象就是window對象,而當函數做爲對象中的一個方法調用時,this等於這個對象(TODO 關於this作一次整理)。因爲匿名函數的做用域是全局性的,所以閉包的this一般指向全局對象window:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        return function(){
            return this.scope;
        }
    }
}

調用object.getScope()()返回值爲global而不是咱們預期的local,前面咱們說過閉包中內部匿名函數會攜帶外部函數的做用域,那爲何沒有取得外部函數的this呢?每一個函數在被調用時,都會自動建立thisarguments,內部匿名函數在查找時,搜索到活躍對象中存在咱們想要的變量,所以中止向外部函數中的查找,也就永遠不可能直接訪問外部函數中的變量了。總之,在閉包中函數做爲某個對象的方法調用時,要特別注意,該方法內部匿名函數的this指向的是全局變量。
幸運的是咱們能夠很簡單的解決這個問題,只須要把外部函數做用域的this存放到一個閉包能訪問的變量裏面便可:

var scope = "global";
var object = {
    scope:"local",
    getScope:function(){
        var that = this;
        return function(){
            return that.scope;
        }
    }
}

object.getScope()()返回值爲local

  • 內存與性能
    因爲閉包中包含與函數運行期上下文相同的做用域鏈引用,所以,會產生必定的負面做用,當函數中活躍對象和運行期上下文銷燬時,因爲必要仍存在對活躍對象的引用,致使活躍對象沒法銷燬,這意味着閉包比普通函數佔用更多的內存空間,在IE瀏覽器下還可能會致使內存泄漏的問題,以下:
function bindEvent(){
    var target = document.getElementById("elem");
    target.onclick = function(){
        console.log(target.name);
    }
 }

上面例子中匿名函數對外部對象target產生一個引用,只要是匿名函數存在,這個引用就不會消失,外部函數的target對象也不會被銷燬,這就產生了一個循環引用。解決方案是經過建立target.name副本減小對外部變量的循環引用以及手動重置對象:

function bindEvent(){
    var target = document.getElementById("elem");
    var name = target.name;
    target.onclick = function(){
        console.log(name);
    }
    target = null;
 }

閉包中若是存在對外部變量的訪問,無疑增長了標識符的查找路徑,在必定的狀況下,這也會形成性能方面的損失。解決此類問題的辦法咱們前面也曾提到過:儘可能將外部變量存入到局部變量中,減小做用域鏈的查找長度。

總結:閉包不是javascript獨有的特性,可是在javascript中有其獨特的表現形式,使用閉包咱們能夠在javascript中定義一些私有變量,甚至模仿出塊級做用域,但閉包在使用過程當中,存在的問題咱們也須要了解,這樣才能避免沒必要要問題的出現。

相關文章
相關標籤/搜索