1. 程式人生 > >從一道簡單面試題來解讀JS中的閉包和作用域

從一道簡單面試題來解讀JS中的閉包和作用域

先上程式碼

var count=10;//全域性作用域 標記為f1
function add(){
    var count=0;//函式全域性作用域 標記為f2
    return function(){
        count+=1;//函式的內部作用域
        alert(count);
    }
}
var s=add()
s();//輸出1
s();//輸出2

加()的返回值是一個函式,首先第一次呼叫()的的時候,是執行加()的返回的函式,也就是下面這個函式:

function(){
        count+=1;//函式的內部作用域
        alert(count);
    }

也就是將計數+ 1,再輸出,那計數是從哪兒來的的呢,根據作用域鏈的規則,底層作用域沒有宣告的變數,會向上一級找,找到就返回,沒找到就一直找,直到視窗的變數,沒有的話就返回undefined。這裡明顯計數是函式內部的f2的那個計數。

如果把F2的計數刪掉,我們再來看

var count=10;//全域性作用域
function add(){
    //var count=0;註釋掉了
    return function(){
        count+=1;//函式的內部作用域
        alert(count);
    }
}
var s=add()
s();//輸出11
s();//輸出12

自然這是體現不出閉包的性質,只為了說明函式作用域鏈。

我們再來看原來的程式碼,第一次執行,是沒有疑問的輸出1,那第二次的過程是怎樣的呢?繼續執行那個函式的返回的方法嗎,還是執行返回方法中的計數+ = 1;然後再輸出計數?這裡問題就來了,不應該繼續向上尋找,找到計數= 0;然後還輸出1嗎?不知道你有沒有注意一個問題,那就是s()執行的是下面這個函式

function(){
        count+=1;//函式的內部作用域
        alert(count);
    }

也就是說加(),只被執行了一次。然後執行兩次S(),那計數的值就是隻聲明瞭一次。

var s = add(),函式add only在這裡執行了一次。

下面執行的都是S(),那第二次的計數的值是從哪兒來的,沒錯它還是第一次執行新增時,留下來的那個變數。

但是函式變數執行完就會被釋放啊,為什麼還在?這裡就是一個垃圾回收機制的引用計數問題。

如果一個變數的引用不為0,那麼他不會被垃圾回收機制回收,引用,就是被呼叫。

由於再次執行s()的時候,再次引用了第一次add()產生的變數計數,所以計數沒有被釋放,第一次s(),count的值為1,第二次執行s(),計數的值再加1,自然就是2了。

讓我們返回來再看看,根據以上所說,如果執行兩次add(),那就應該輸出都是1,來改一下這個函式

function add(){
    var count=0;//函式全域性作用域
    return function(){
        count+=1;//函式的內部作用域
        alert(count);
    }
}
add()();//輸出1
add()();//輸出1

如之前所想的一樣,輸出的兩次都是1。

另外,用一句話來解釋閉包,“閉包就是能夠讀取其他函式內部變數的函式”。

說道閉包,就必然會扯出函式的作用域

ES5變數的作用域無非就是兩種:全域性變數和區域性變數,JS語言的特殊之處,就在於函式內部可以直接讀取全域性變數。

var n=1; 
  function f1(){
alert(n);
		}
f1(); // 1
function f1(){
  var n=1;
}
alert(n); // error

這裡有一個地方需要注意,函式內部宣告變數的時候,一定要使用VAR命令。如果不用的話,你實際上聲明瞭一個全域性變數。

function f1(){
 n=1;
}
f1();
alert(n); // 1

有時候需要得到函式內的區域性變數,在函式的內部,再定義一個函式。

function f1(){
  n=1;
  function f2(){
        alert(n); // 1
       }
}

在上面的程式碼中,函式f2就被包括在函式f1內部,這時f1內部的所有區域性變數,對f2都是可見的。但是反過來就不行,f2內部的區域性變數,對f1就是不可見的。這就是Java指令碼語言特有的“鏈式作用域”結構,子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

既然F2可以讀取F1中的區域性變數,那麼只要把F2作為返回值就可以在F1外部讀取它的內部變量了。

function f1(){
  n=1;
  function f2(){
       alert(n);
       }

 return f2;

}
var result=f1();
result(); // 1

使用閉包的注意點

1)由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在IE中可能導致記憶體洩露。解決方法是,在退出函式之前,將不使用的區域性變數全部刪除。

2)閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(物件)使用,把閉包當作它的公用方法,把內部變數當作它的私有屬性,這時一定要小心,不要隨便改變父函式內部變數的值。

閉包的用途

閉包可以用在許多地方。它的最大用處有兩個,的英文一個前面提到的可以讀取函式內部的變數,就是另一個讓這些變數的值始終保持在記憶體中

怎麼來理解這句話呢?請看下面的程式碼。

function f1(){
     var n=1;
     Add=function(){n+=1}
      function f2(){
      alert(n);
	}

 return f2;
}
var result=f1();
result(); // 1
Add();
result(); // 2

在這段程式碼中,導致實際上就是閉包F2函式。它一共運行了兩次,第一次的值是1,第二次的值是2,這證明了,函式F1中的區域性變數Ñ一直儲存在記憶體中,並沒有在F1呼叫後被自動清除。

為什麼會這樣呢?原因就在於F1是F2的父函式,而F2被賦給了一個全域性變數,這導致F2始終在記憶體中,而F2的存在依賴於F1,因此F1也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制回收。

這段程式碼中另一個值得注意的地方,就是“Add = function(){n + = 1}”這一行,首先在Add前面沒有使用var關鍵字,因此新增是一個全域性變數,而不是區域性變數。其次,新增的值是一個匿名函式,而這個匿名函式本身也是一個閉包,所以新增相當於是一個二傳手,可以在函式外部對函式內部的區域性變數進行操作。