作用域在JS中同樣也是一個重要的概念。它不復雜,因為ES5中只有全局作用域和函數作用域,我們都知道他沒有塊級作用域。但在ES6中多了一個let,他可以保證外層塊不受內層塊的影響。即內層塊形成了一個塊級作用域,這是let的一個特點。它不簡單,因為在許多的函數嵌套的情景下,只有對它理解深刻,才能更好的去分析。今天我們着重講的是函數作用域與全局作用域。
同樣在分析之前,我們來看一段代碼。
var a=1; function f1(){ var b=2; function f2(){ var c=b; b=a; a=c; console.log(a,b,c); } f2(); } f1();//2,1,2
上面的代碼,有三個執行上下文環境(EC),全局EC,f1EC,f2EC。全局環境下有一個變量a和一個函數f1(),在f1環境中,有一個變量b和一個函數f2(),在f2環境中有一個變量c。但在f2中,可以訪問到f1環境中的b,也可以訪問到全局環境中的a,在f1中,可以訪問到全局環境下的a,但不可以訪問f2中的c,在全局中,不可以訪問f1中的b也不可以訪問f2中的c.。這就是一個作用域鏈。
於是,我們可以知道,函數的內部環境可以通過作用域鏈訪問到所有的外部環境,但是外部環境卻不可以訪問外部環境,這就是作用域的關鍵。但是我們要知道,作用域是在一個函數創建時就已經形成的,而不是調用時。所以有些人可能會認為按着作用域鏈向上查找是查找它的父作用域,就像上面的那個例子。但是這個例子只是一種特殊情況。我們要認識到並不是查找它的父作用域,而是查找創建該函數的那個作用域。看下面的這段代碼。
var a=10; function fn(){ var a=20; return function b(){ console.log(a); }; } var g=fn(); g();//20
這里我們在調用g函數,發現他的值是20,。而它所謂的父作用域應該是全局作用域,但它卻不是10.所以這就說明了作用域鏈向上查找是尋找創建它的那個作用域。
上面的這個例子同時引出了我下面要說明的一個問題,閉包。
閉包,是函數中一個核心的概念。它的文字說明多種多樣,我看過很多人對它的文字描述,雖然說得不全一樣,但是中心觀點都差不太多。我個人認為我最喜歡的一個文字描述就是《鋒利的jquery》關於插件描述的那個章節中的一段說明。
閉包,允許使用內部函數(即函數定義和函數表達式位於另一個函數的函數體內),而且,這些內部函數可以訪問他們所在的外部函數中的聲明的所有局部變量丶參數和聲明的其他內部函數,當其中一個這樣的內部函數在包含他們的外部函數之外被調用時,就會形成閉包。即內部函數會在外部函數返回后被執行。而當這個內部函數執行時,它仍然必須訪問其外部函數的局部變量丶參數以及其他內部函數。這些局部變量丶參數和函數聲明(最初時)的值是外部函數返回時的值,但也會受到內部函數的影響。
上面這段話就是《鋒利的jquery》中關於閉包的一段描述。說的很長很詳細,簡單來說,就是在一個函數a內部定義的另一個函數b,當b在a之外被執行時,就會形成閉包。同時b函數仍然可以訪問到a函數中的局部變量與函數。
我們在開始了解閉包時,有一個特別經典的題目。看下面代碼
function fn(){ var array=[]; for(var i=0;i<10;i++){ array[i]=function(){ return i; } } return array; } fn();//[ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ, ƒ]
我們的本意是得到這個數組中每個函數都能返回自己的索引值,可是得到的是每個函數卻都返回了10.如上面的文字說明中所講的那樣,閉包保存的是定義它的那個函數內部的局部變量丶參數和其他內部函數,也就是說保存的是這個函數執行上下文中的整個VO,而不是一個變量。上面代碼中的函數作用域鏈中都保存着fn的活動對象,他們引用的都是一個i,當fn返回時,i的值是10,所以每個函數都引用保存i那個變量的同一個變量。我們如果想得到原先想得到的那個結果,可以加上另一個匿名函數改變他的父作用域(其實應該是創建它的作用域),將它包裹起來。
function fn(){ var array=[]; for(var i=0;i<10;i++){ array[i]=function(num){ return function(){ return num; }; }(i); } return array; }
這個匿名函數有一個參數num,同時是返回值。在調用每個匿名函數時,傳入了變量i。由於參數是按值傳遞的,所以i就會復制給num,而這個匿名函數的內部又創建了一個訪問num的閉包,返回后能夠訪問到該匿名函數中的VO(包括參數),於是每個函數返回的都是num的一個副本,所以可以得到不同的值。
其實,說了這么多,我們只要熟悉閉包的兩個應用場景,就能比較好的理解閉包的意義。
一.作為函數的返回值.。作為函數返回值被執行后仍然可以訪問定義它的那個函數環境的VO。
function f(){ var a=1; return function(){ console.log(a); } } var g=f(); g();//1;
二.作為一個函數的參數。作為函數返回值被當做另一個函數的參數傳入時,仍然是訪問定義它的那個函數環境的VO
function f(){ var a=1; return function(){ console.log(a); } } var g=f(); g();//1; function F(fn){ var a=2; fn(); } F(g);//1
上面兩個小例子也正好說明了閉包可以訪問定義它的那個函數作用域下的內部變量和內部函數。其實是整個VO,所以還包含參數。
閉包的理解差不多就是這樣,遇到比較復雜的情況我們只要按着定義慢慢的一步步的尋找,一切問題都能迎刃而解。