神馬是閉包
關於閉包的概念,是婆說婆有理。
閉包是指有權訪問另外一個函數作用域中的變量的函數
這概念有點繞,拆分一下。從概念上說,閉包有兩個特點:
在ES 6之前,Javascript只有函數作用域的概念,沒有塊級作用域(但catch捕獲的異常 只能在catch塊中訪問)的概念(IIFE可以創建局部作用域)。每個函數作用域都是封閉的,即外部是訪問不到函數作用域中的變量。
function getName() { var name = "美女的名字"; console.log(name); //"美女的名字" } function displayName() { console.log(name); //報錯 }
但是為了得到美女的名字,不死心的單身汪把代碼改成了這樣:
function getName() { var name = "美女的名字"; function displayName() { console.log(name); } return displayName; } var 美女 = getName(); 美女() //"美女的名字"
這下,美女是一個閉包了,單身汪想怎麼玩就怎麼玩了。(但並不推薦單身汪用中文做變量名的寫法,大家不要學)。
關於閉包呢,還想再說三點:
1、閉包可以訪問當前函數以外的變量
function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //訪問外部的date } return getDate('今天是:'); //"今天是:815" } getOuter();
getDate是一個閉包,該函數執行時,會形成一個作用域A,A中並沒有定義變量date,但它能在父一級作用域中找到該變量的定義。
2、即使外部函數已經返回,閉包仍能訪問外部函數定義的變量
function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //訪問外部的date } return getDate; //外部函數返回 } var today = getOuter(); today('今天是:'); //"今天是:815" today('明天不是:'); //"明天不是:815"
3、閉包可以更新外部變量的值
function updateCount(){ var count = 0; function getCount(val){ count = val; console.log(count); } return getCount; //外部函數返回 } var count = updateCount(); count(815); //815 count(816); //816
作用域鏈
為毛閉包就能訪問外部函數的變量呢?這就要說說Javascript中的作用域鏈了。
Javascript中有一個執行環境(execution context)的概念,它定義了變量或函數有權訪問的其它數據,決定了他們各自的行為。每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。你可以把它當做Javascript的一個普通對象,但是你只能修改它的屬性,卻不能引用它。
變量對象也是有父作用域的。當訪問一個變量時,解釋器會首先在當前作用域查找標示符,如果沒有找到,就去父作用域找,直到找到該變量的標示符或者不再存在父作用域了,這就是作用域鏈。
作用域鏈和原型繼承有點類似,但又有點小區別:如果去查找一個普通對象的屬性時,在當前對象和其原型中都找不到時,會返回undefined;但查找的屬性在作用域鏈中不存在的話就會拋出ReferenceError。
作用域鏈的頂端是全局對象。對於全局環境中的代碼,作用域鏈只包含一個元素:全局對象。所以,在全局環境中定義變量的時候,它們就會被定義到全局對象中。當函數被調用的時候,作用域鏈就會包含多個作用域對象。
關於作用域鏈講得略多(紅皮書上有關於作用域及執行環境的詳細解釋),看一個簡單地例子:
// my_script.js "use strict"; var foo = 1; var bar = 2;
在全局環境中,創建了兩個簡單地變量。如前面所說,此時變量對象是全局對象。
改動一下代碼,創建一個沒有函數嵌套的函數:
"use strict"; var foo = 1; var bar = 2; function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc"); } console.log("outside"); //-- and then, call it: myFunc();
當myFunc被定義的時候,myFunc的標識符(identifier)就被加到了當前的作用域對象中(在這裡就是全局對象),並且這個標識符所引用的是一個函數對象(function object)。函數對象中所包含的是函數的源代碼以及其他的屬性。其中一個我們所關心的屬性就是內部屬性[[scope]]。[[scope]]所指向的就是當前的作用域對象。也就是指的就是函數的標識符被創建的時候,我們所能夠直接訪問的那個作用域對象(在這裡就是全局對象)。
比較重要的一點是:myFunc所引用的函數對象,其本身不僅僅含有函數的代碼,並且還含有指向其被創建的時候的作用域對象。
當myFunc函數被調用的時候,一個新的作用域對象被創建了。新的作用域對象中包含myFunc函數所定義的本地變量,以及其參數(arguments)。這個新的作用域對象的父作用域對象就是在運行myFunc時我們所能直接訪問的那個作用域對象。
如前面所說,當函數返回沒有被引用的時候,就會被垃圾回收器回收。但是對於閉包(函數嵌套是形成閉包的一種簡單方式)呢,即使外部函數返回了,函數對象仍會引用它被創建時的作用域對象。
"use strict"; function createCounter(initial) { var counter = initial; function increment(value) { counter += value; } function get() { return counter; } return { increment: increment, get: get }; } var myCounter = createCounter(100); console.log(myCounter.get()); // 返回 100 myCounter.increment(5); console.log(myCounter.get()); // 返回 105
當調用createCounter(100)時,內嵌函數increment和get都有指向createCounter(100) scope的引用。如果createCounter(100)沒有任何返回值,那麼createCounter(100) scope不再被引用,於是就可以被垃圾回收。但是因為createCounter(100)實際上是有返回值的,並且返回值被存儲在了myCounter中,所以對象之間的引用關系發生變化。
需要用點時間思考的是:即使createCounter(100)已經返回,但是其作用域仍在,並能且只能被內聯函數訪問。可以通過調用myCounter.increment() 或 myCounter.get()來直接訪問createCounter(100)的作用域。
當myCounter.increment() 或 myCounter.get()被調用時,新的作用域對象會被創建,並且該作用域對象的父作用域對象會是當前可以直接訪問的作用域對象。
當執行到return counter;時,在get()所在的作用域並沒有找到對應的標示符,就會沿著作用域鏈往上找,直到找到變量counter,然後返回該變量,調用increment(5)則會更有意思。當單獨調用increment(5)時,參數value會存貯在當前的作用域對象。函數要訪問value,能馬上在當前作用域找到該變量。但是當函數要訪問counter時,並沒有找到,於是沿著作用域鏈向上查找,在createCounter(100)的作用域找到了對應的標示符,increment()就會修改counter的值。除此之外,沒有其他方式來修改這個變量。閉包的強大也在於此,能夠存貯私有數據。
Similar function objects, different scope objects
對於上面的counter示例,再說點擴展的事。看代碼:
//myScript.js "use strict"; function createCounter(initial) { /* ... see the code from previous example ... */ } //-- create counter objects var myCounter1 = createCounter(100); var myCounter2 = createCounter(200);
myCounter1 和 myCounter2創建之後,關系圖是醬紫的:
在上面的例子中,myCounter1.increment和myCounter2.increment的函數對象擁有著一樣的代碼以及一樣的屬性值(name,length等等),但是它們的[[scope]]指向的是不一樣的作用域對象。
這才有了下面的結果:
var a, b; a = myCounter1.get(); // a 等於 100 b = myCounter2.get(); // b 等於 200 myCounter1.increment(1); myCounter1.increment(2); myCounter2.increment(5); a = myCounter1.get(); // a 等於 103 b = myCounter2.get(); // b 等於 205
作用域和this
作用域會存儲變量,但this並不是作用域的一部分,它取決於函數調用時的方式。關於this指向的總結,可以看這篇文章:JavaScript面試問題:事件委托和this