【JS腳丫系列】重溫閉包

閉包概念解釋:

閉包(也叫詞法閉包或者函數閉包)。數組

在一個函數parent內聲明另外一個函數child,造成了嵌套。函數child使用了函數parent的參數或變量,那麼就造成了閉包。瀏覽器

閉包(closure)是能夠訪問外部函數做用域中的變量或參數的函數。閉包

此時,包裹的函數稱爲外部函數。內部的稱爲內部函數或閉包函數。(zyx456自定義:或稱爲父函數和子函數)。app

閉包wiki函數

JS採用詞法做用域(lexical scoping),函數的執行依賴於函數做用域,這個做用域是在函數定義時決定的,而不是函數調用時決定的。性能

詞法做用域:詞法做用域也叫靜態做用域,是指做用域在詞法解析階段就已經肯定了,不會改變。
動態做用域:是指做用域在運行時才能肯定。this

參看下面的例子,引自楊志的回答code

var foo=1;

function static(){
    alert(foo);
}

!function(){
    var foo=2;
    
    static();
}();

在js中,會彈出1而非2,由於static的scope在建立時,記錄的foo是1。
若是js是動態做用域,那麼他應該彈出2

zyx456:識別閉包,在詞法分析階段已經肯定了。對象

當外部函數運行的時候,一個閉包就造成了,他由內部函數的代碼以及任何內部函數中指向外部函數局部變量的引用組成。生命週期

注意事項

01,閉包函數做用域中,使用的外部函數變量不會被馬上銷燬回收,因此會佔用更多的內存。過分使用閉包會致使性能降低。建議在很是有必要的時候才使用閉包。

02,同一個閉包函數,所訪問的外部函數的變量是同一個變量。

03,若是把閉包函數,賦值給不一樣的變量,那麼不一樣的變量指向的是不一樣的閉包函數,所使用的外部函數變量是不一樣的。

04,閉包函數分爲定義時,和運行時。只有運行時,纔會訪問外部函數的變量。

05,在for循環的閉包函數,只有在運行時,纔在做用域中尋找變量。for循環會先運行完畢,此時,閉包函數並無運行。

06,若是在for循環中,使用閉包的自執行函數。那麼閉包會使用for循環的變量i(0-*,假設i從0開始)。

07,一個函數裏,能夠有多個閉包。

匿名自執行函數,能夠封裝私有變量。不會污染全局做用域。匿名函數中定義的任何變量,都會在執行結束時被銷燬。

eval+with(僅瞭解)

在評論中賀師俊還提到,eval 和 with能夠產生動態做用域的效果:

好比 with(o) { console.log(x) } 這個x實際有多是 o.x 。因此這就不是靜態(詞法)做用域了。

var x = 0;
void function (code) {
    eval(code);
    console.log(x)
}('var x=1')

不過注意eval在strict模式下被限制了,再也不能造成動態做用域了。

爲何閉包函數能夠訪問外部函數的變量?

由於閉包函數的做用域鏈包含了外部函數的做用域。

如何建立閉包?

在一個函數類內建立另一個函數。內部函數使用了外部函數的變量,就造成了閉包。

普通函數的內部函數是閉包函數麼?

zyx456:不是。


函數第一次被調用時,會發生什麼?

當函數第一次被調用時,會建立一個執行環境(execution context)和相應的做用域鏈,並把做用域鏈賦值給一個內部屬性(即[[Scope]])。

而後,使用this、arguments和其餘參數來初始化函數的活動對象(activation object)。

在做用域鏈中,內部函數的活動對象處於第一位,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,……直至做爲做用域鏈終點的全局執行環境。

在函數執行過程當中,讀取和寫入變量的值,都須要在做用域鏈中查找變量。

每次調用JS函數,會爲之建立一個新的對象來保存全部的局部變量(函數定義的變量,函數參數。),把這個對象添加到做用域鏈中。函數體內部的變量都保存在函數做用域內。

咱們將做用域鏈看作一個對象列表,而不是一個棧。

(zyx456:棧是一種線性表,僅容許在表的一端進行插入和刪除操做。)

當函數返回的時候,就從做用域鏈中將這個綁定變量的對象刪除。

若是這個函數不存在嵌套的函數,也沒有其餘引用指向這個綁定變量的對象,它就會被當作垃圾回收掉。

(zyx456:這個操做由瀏覽器自動完成)。

若是這個函數有嵌套的函數,每一個嵌套的函數都各自對應一個做用域鏈。

這時:

  • 內部函數,被做爲返回值返回,或存儲在某處屬性中,就是會有一個外部引用指向這個它,那麼它就不會被當作垃圾回收,而且它所使用外部變量所在的對象也不會被當作垃圾回收。只有內部函數被銷燬後,外部函數的活動對象纔會被銷燬。

在函數中訪問一個變量時,就會從做用域鏈中查找變量。

通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域(全局執行環境的環境對象)。

可是,閉包的狀況又有所不一樣。閉包函數的做用域鏈上有外部函數的做用域鏈。因此閉包函數能夠訪問外部函數的變量。

閉包函數必須返回(return)麼,return這個閉包函數?

zyx456:沒必要要返回,只要使用外部函數的變量便可。

代碼:

function fn1() {
    var a = 1;
    function fn2() {
        console.log(a);
    }
    fn2();
}
fn1();

若是用不一樣的變量引用函數中的閉包函數,那麼是不一樣的閉包變量。

簡單的例子:

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

運行結果爲:
0
1
0
1

閉包的用途:

能夠建立私有變量。

由於只有閉包函數能夠訪問外部函數的變量。

由於在閉包內部保持了對外部活動對象的訪問,但外部的變量卻沒法直接訪問內部,避免了全局污染;

function setMoyu(){
    var name = "moyu";
    return function(newValue){
        name=newValue;
        console.log(name);
        
    }
}

var setValue = setMoyu();
setValue("world");//world
/*zyx456:這時name是私有屬性了,只能經過閉包函數設置它*/

閉包的缺點?

  1. 可能致使內存佔用過多,由於閉包攜帶了自身的函數做用域。
  2. 閉包只能取得外部函數中的最後一個值。

做用域:

變量聲明若是不使用 var 關鍵字,那麼它就是一個全局變量,即使它在函數內定義。

變量生命週期

全局變量的做用域是全局性的,即在整個JS程序中,全局變量到處都在。

而在函數內部聲明的變量,只在函數內部起做用。

這些變量是局部變量,做用域是局部性的;

函數的參數也是局部性的,只在函數內部起做用。

在JS中,全部函數都能訪問它們上一層的做用域。

例子:

function compare(value1, value2){
    if (value1 < value2){
        return -1;
    } else if (value1 > value2){
        return 1;
    } else {
        return 0;
    }
}

var result = compare(5, 10);

內存泄漏

因爲IE 的JS對象和DOM對象使用不一樣的垃圾收集方式,所以閉包在IE中會致使內存泄漏的問題,也就是沒法銷燬駐留在內存中的元素。

事件綁定種的匿名函數也是閉包函數。

若是閉包的做用域鏈中保存着一個HTML元素,那麼就意味着該元素將沒法被銷燬。

function box() {
    var oDiv = document.getElementById('oDiv'); //oDiv 用完以後一直駐留在內存
    oDiv.onclick = function () {
        alert(oDiv.innerHTML); //這裏用oDiv 致使內存泄漏
    };
}
box();

那麼在最後應該將oDiv 解除引用來避免內存泄漏。

function box() {
    var oDiv = document.getElementById('oDiv');
    var text = oDiv.innerHTML;
    oDiv.onclick = function () {
        alert(text);
    };
    oDiv = null; //解除引用
}

PS:若是並無使用解除引用,那麼須要等到瀏覽器關閉才得以釋放。

閉包和this和arguments

閉包函數中的this問題

對於某個函數來講,若是函數在全局環境中,this指向window。若是在對象中,就指向這個對象。

而對象中的閉包函數,this指向window。由於閉包並不屬於這個對象的屬性或方法。

var user = 'The Window';
var obj = {
    user : 'The Object',
    getUserFunction : function () {
        return function () { //閉包不屬於obj,裏面的this 指向window
            return this.user;
        };
    }
};
alert(obj.getUserFunction()()); //The window
//能夠強制指向某個對象
alert(obj.getUserFunction().call(obj)); //The Object
//也能夠從上一個做用域中獲得對象
getUserFunction : function () {
    var that = this; //從對象的方法裏得對象
    return function () {
        return that.user;
    };
}

例子:

var self = this; // 將this保存至一個變量中,以便嵌套的函數可以訪問它

綁定arguments的問題與之相似。

arguments並非一個關鍵字,但在調用每一個函數時都會自動聲明它,因爲閉包具備本身所綁定的arguments,所以閉包內沒法直接訪問外部函數的參數數組,除非外部函數將參數數組保存到另一個變量中:

var outerArguments = arguments;  //保存起來以便嵌套的函數能使用它

在經過call()或apply()改變函數執行環境的狀況下,this就會指向其餘對象。

例子:

var scope = "global scope";             // 全局變量
function checkscope() {
        var scope = "local scope";      // 局部變量
        function f() { return scope; }  // 在做用域中返回這個值
        return f();
}
checkscope()                            // => "local scope"

checkscope()函數聲明瞭一個局部變量,並定義了一個函數f(),函數f()返回了這個變量的值,最後將函數f()的執行結果返回。

你應當很是清楚爲何調用checkscope()會返回"local scope"。如今咱們對這段代碼作一點改動。

var scope = "global scope";             // 全局變量
function checkscope() {
        var scope = "local scope";      // 局部變量
        function f() { return scope; }  // 在做用域中返回這個值
        return f;
}
checkscope()()                          // 返回值是什麼?//local scope
相關文章
相關標籤/搜索