進擊的 JavaScript(四) 之 閉包

上一節說了執行上下文,這節我們就乘勝追擊來搞搞閉包!頭疼的東西讓你再也不頭疼!瀏覽器

1、函數也是引用類型的。

function f(){ console.log("not change") };

var ff = f;
 
function f(){ console.log("changed") };

ff();

//"changed"
//ff 保存着函數 f 的引用,改變f 的值, ff也變了

//來個對比,估計你就明白了。
var f = "not change";

var ff = f;

f = "changed";

console.log(ff);

//"not change"
//ff 保存着跟 f 同樣的值,改變f 的值, ff 不會變

其實,就是引用類型 和 基本類型的 區別。閉包

<br/>函數

2、函數建立一個參數,就至關於在其內部聲明瞭該變量

function f(arg){
    console.log(arg)
}

f();

//undefined

function f(arg){
    arg = 5;
    console.log(arg);
}
f();

//5

<br/>this

3、參數傳遞,就至關於變量複製(值的傳遞)

基本類型時,變量保存的是數據,引用類型時,變量保存的是內存地址。參數傳遞,就是把變量保存的值 複製給 參數。設計

var o = { a: 5 };

function f(arg){
    arg.a = 6;
}

f(o);

console.log(o.a);
//6

<br/>code

4、垃圾收集機制

JavaScript 具備自動垃圾收集機制,執行環境會負責管理代碼執行過程當中使用的內存。函數中,正常的局部變量和函數聲明只在函數執行的過程當中存在,當函數執行結束後,就會釋放它們所佔的內存(銷燬變量和函數)。對象

而js 中 主要有兩種收集方式:ip

  1. 標記清除(常見) //給變量標記爲「進入環境」 和 「離開環境」,回收標記爲「離開環境」的變量。
  2. 引用計數 // 一個引用類型值,被賦值給一個變量,引用次數加1,經過變量取得引用類型值,則減1,回收爲次數爲0 的引用類型值。

知道個大概狀況就能夠了,《JavaScript高級程序設計 第三版》 4.3節 有詳解,有興趣,能夠看下。.內存

<br/>作用域

5、做用域

以前說過,JavaScript中的做用域無非就是兩種:全局做用域局部做用域
根據做用域鏈的特性,咱們知道,做用域鏈是單向的。也就是說,在函數內部,能夠直接訪問函數外部和全局變量,函數。可是,反過來,函數外部和全局,是訪問不了函數內的變量,函數的。

function testA(){
    var a = 666;
}
console.log(a);

//報錯,a is not defined

var b = 566;
function testB(){
    console.log(b);
}

//566

可是,有時候,咱們須要在函數外部 訪問函數內部的變量,函數。通常狀況下,咱們是辦不到的,這時,咱們就須要閉包來實現了。

<br/>

6、開始閉包!

function fa(){
    var va = "this is fa";

    function fb(){
        console.log(va);
    }
    
    return fb;
}

var fc = fa();
fc();

//"this is fa"

想要讀取fa 函數內的變量 va,咱們在內部定義了一個函數 fb,可是不執行它,把它返回給外部,用 變量fc接受。此時,在外部再執行fc,就讀取了fa 函數內的變量 va

<br/>

7、閉包的概念

其實,簡單點說,就是在 A 函數內部,存在 B 函數, B函數 在 A 函數 執行完畢後再執行。B執行時,訪問了已經執行完畢的 A函數內部的變量和函數。

由此可知:閉包是函數A的執行環境 以及 執行環境中的函數 B組合而構成的。

上篇文章中說過,變量等 都儲存在 其所在執行環境的活動對象中,因此說是 函數A 的執行環境。

當 函數A執行完畢後,函數B再執行,B的做用域中就保留着 函數A 的活動對象,所以B中能夠訪問 A中的 變量,函數,arguments對象。此時產生了閉包。大部分書中,都把 函數B 稱爲閉包,而在谷歌瀏覽器中,把 A函數稱爲閉包。

<br/>

8、閉包的本質

以前說過,當函數執行完畢後,局部活動對象就會被銷燬。其中保存的變量,函數都會被銷燬。內存中僅保存全局做用域(全局執行環境的變量對象)。可是,閉包的狀況就不一樣了。

以上面的例子來講,函數fb 和其所在的環境 函數fa,就組成了閉包。函數fa執行完畢後,按道理說, 函數fa 執行環境中的 活動對象就應該被銷燬了。可是,由於 函數fa 執行時,其中的 函數fb 被 返回,被 變量fc 引用着。致使,函數fa 的活動對象沒有被銷燬。而在其後 fc() 執行,就是 函數fb 執行時,構建的做用域中保存着 函數fa 的活動對象,所以,函數fb 中 能夠經過做用域鏈訪問 函數fa 中的變量。

我已經盡力地說明白了。就看各位的了。哈哈!其實,簡單的說:就是fa函數執行完畢了,其內部的 fb函數沒有執行,並返回fb的引用,當fb再次執行時,fb的做用域中保留着 fa函數的活動對象。

再來個有趣經典的例子:

for (var i=1; i<=5; i++) {
    setTimeout(function(){
        console.log(i);
    },i*1000);
}

//每隔一秒輸出一個6,共5個。

是否是跟你想的不同?其實,這個例子重點就在setTimeout函數上,這個函數的第一個參數接受一個函數做爲回調函數,這個回調函數並不會當即執行,它會在當前代碼執行完,並在給定的時間後執行。這樣就致使了上面狀況的發生。

能夠下面對這個例子進行變形,能夠有助於你的理解把:

var i = 1;
while(i <= 5){
    setTimeout(function(){
        console.log(i);
    },i*1000)
     
    i = i+1;
}

正由於,setTimeout裏的第一個函數不會當即執行,當這段代碼執行完以後,i 已經 被賦值爲6了(等於5時,進入循環,最後又加了1),因此 這時再執行setTimeout 的回調函數,讀取 i 的值,回調函數做用域內沒有i,向上讀取,上面做用域內i的值就是6了。可是 i * 1000,是當即執行的,因此,每次讀的 i 值 都是對的。

這時候,就須要利用閉包來保存每一個循環時, i 不一樣的值。

function makeClosures(i){     //這裏就和 內部的匿名函數構成閉包了
    var i = i;    //這步是不須要的,爲了讓看客們看的輕鬆點
    return function(){
        console.log(i);     //匿名沒有執行,它能夠訪問i 的值,保存着這個i 的值。
    }
}

for (var i=1; i<=5; i++) {
    setTimeout(makeClosures(i),i*1000);  
    
    //這裏簡單說下,這裏makeClosures(i), 是函數執行,並非傳參,不是一個概念
    //每次循環時,都執行了makeClosures函數,都返回了一個沒有被執行的匿名函數
    //(這裏就是返回了5個匿名函數),每一個匿名函數都是一個局部做用域,保存着每次傳進來的i值
    //所以,每一個匿名函數執行時,讀取`i`值,都是本身做用域內保存的值,是不同的。因此,就獲得了想要的結果
}

//1
//2
//3
//4
//5

閉包的關鍵就在,外部的函數執行完畢後,內部的函數再執行,並訪問了外部函數內的變量。

你可能在別處,或者本身想到了下面這種解法:

for (var i=1; i<=5; i++) {
    (function(i){
        setTimeout(function(){
            console.log(i);
        },i*1000);
    })(i);
}

若是你一直把這個當作閉包,那你可能看到的是不一樣的閉包定義吧(犀牛書和高程對閉包的定義不一樣)。嚴格來講,這不是閉包,這是利用了當即執行函數函數做用域 來解決的。

作下變形,你再看看:

for (var i=1; i<=5; i++) {
    function f(i){
        setTimeout(function(){
            console.log(i);
        },i*1000);
    };
    
    f(i);
}

這樣看就很明顯了吧,主要是利用了函數做用域,而使用當即執行函數,是爲了簡化步驟。

總結:判斷是否是閉包,我總結了要知足如下三點:

  1. 兩個函數。有內函數 和 外函數。
  2. 外函數執行完畢後,內函數 尚未執行。
  3. 當內函數執行時(經過外部引用或者返回內函數),訪問了 外函數內部的 變量,函數等(說是訪問,其實內函數保存着外函數的活動對象,所以,arguments對象也能夠訪問到)。

<br/>
<br/>

附錄:

其實這道題,知道ES6let 關鍵詞,估計也想到了另外一個解法:

for (let i=1; i<=5; i++) {   //這裏的關鍵就是使用的let 關鍵詞,來造成塊級做用域
    setTimeout(function(){
        console.log(i);
    },i*1000);
}

我不知道,你們有沒有疑惑啊,爲啥使用了塊級做用域就能夠了呢。反正我當初就糾結了半天。

11月 2日修正:

這個答案的關鍵就在於 塊級做用域的規則了。它讓let 聲明的變量只在{}內有效,外部是訪問不了的。

作下變形:

for (var i=1; i<=5; i++) {
    let j = i;
    setTimeout(function(){
        console.log(j);
    },j*1000);
}

其實,for 循環時,每次都會用let 或者 var 建立一個新變量,並以以前迭代中同名變量的值將其初始化。而這裏正由於使用let,致使每次循環都會建立一個新的塊級做用域,這樣,雖然setTimeout 中的匿名函數內沒有 i 值,但它向上做用域讀取i 值,就讀到了塊級做用域內 i 的值。

上面用當即執行函數模擬塊級做用域,就是這個道理啦!

相關文章
相關標籤/搜索