神馬是閉包
關於閉包的概念,是婆說婆有理。
閉包是指有權訪問另外一個函數範圍中的變數的函數
這概念有點繞,拆分一下。從概念上說,閉包有兩個特點:
在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); //815count(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()); // 返回 100myCounter.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 objectsvar myCounter1 = createCounter(100);var myCounter2 = createCounter(200);
myCounter1 和 myCounter2建立之後,關係圖是醬紫的:
在上面的例子中,myCounter1.increment和myCounter2.increment的函數對象擁有著一樣的代碼以及一樣的屬性值(name,length等等),但是它們的[[scope]]指向的是不一樣的範圍對象。
這才有了下面的結果:
var a, b;a = myCounter1.get(); // a 等於 100b = myCounter2.get(); // b 等於 200myCounter1.increment(1);myCounter1.increment(2);myCounter2.increment(5);a = myCounter1.get(); // a 等於 103b = myCounter2.get(); // b 等於 205
範圍和this
範圍會儲存變數,但this並不是範圍的一部分,它取決於函數調用時的方式。關於this指向的總結,可以看這篇文章:JavaScript面試問題:事件委託和this