javascript里的作用域是理解javascript語言的關鍵所在,正確使用作用域原理才能寫出高效的javascript代碼,很多javascript技巧也是圍繞作用域進行的,今天我要總結一下關于javascript作用域的相關知識。
很多人使用javascript時候會把{}作為作用域的邊界,所以我們可以看看下面的代碼:
function ftn01(){ var i = 1; if (i == 1){ var a = "ok"; } console.log("a = " + a);// a = ok {var b = "bok";} console.log("b = " + b);// b = bok } ftn01();
我們發現變量a和b都能被打印出來,這就說明if下的{}和單獨的{}并不能保護變量a和b的作用域范圍,我們將上面的代碼修改下:
function ftn01(){ var i = 1; if (i == 1){ var a = "ok"; } console.log("a = " + a);// a = ok {var b = "bok";} console.log("b = " + b);// b = bok } console.log(a); ftn01();
在firebug里會報出如下的錯誤:
可見函數的{}是可以保護變量的作用域的。
其實在javascript語言里只有函數是可以提供作用域,換句話說javascript里有且只有函數作用域,沒有其他的作用域。因此要理解作用域必須從函數講起,javascript里的函數同時也是一個對象,函數同時也是對象這句話讓很多初學者誤解,javascript這個特性和其他很多語言不太一樣,要解釋這個問題就必須從javascript創建函數的機制,javascript語言里有一個對象叫做Function,所有的函數都是該對象的實例,下面我們看看javascript里創建函數的三種方式:
var add01 = function add(a,b){ return a + b; } var add02 = function(a,b){ return a+ b; } var add03 = new Function("a","b","return a + b"); console.log(add01(1,2));// 3 console.log(add02(1,2));// 3 console.log(add03(1,2));// 3 console.log(typeof add01);//function console.log(typeof add02);//function console.log(typeof add03);//function
第一種方式叫做命名函數方式,第二種叫做匿名函數方式,第三種是直接使用Function對象來定義函數,三種方式是等價的,并且都可以用一個變量保存該函數。其實前兩種創建函數的方式是第三種的變種,由此可以看到所有函數都是Function對象的實例。
函數的作用域功能是由函數內置的一個屬性scope體現的,scope屬性是一個類數組的集合,這個類數組的集合叫做函數的作用域鏈,作用域是函數的一個屬性,作用域鏈其實就是該屬性的數據類型。當我們創建一個函數時候,就是上面定義函數add01時候,函數的scope屬性就會被同步創建,并且作用域鏈也會被構建,上面的例子里創建的函數都是屬于全局的,因此作用域鏈只有一個元素即該類數組的length是1,類數組所包含的元素也是一個鍵值對類型,該元素包含所有全局變量例如:window,document,add01等等。
上面的例子里add01是函數的標識符,當add01(1,2)加上了小括號的時候就是執行該函數了,執行一個函數時候,函數會創建一個運行期上下文,英文全稱是:execution context,運行期上下文是函數的一個內部對象和作用域一樣也是不能被外部訪問的,每個運行期的函數都會有一個自己獨有的運行上下文,當函數執行完畢后,該函數的運行上下文也會被銷毀。當函數執行的時候,首先會初始化函數自帶的scope屬性,然后將函數自帶的scope里的作用域鏈復制到運行期上下文里,運行期上下文里也包含一個作用域的屬性,該屬性也是用一個作用域鏈的類數組表示,復制出來的函數的作用域鏈會放到運行期上下文的作用域鏈里,這一步做好后,運行期上下文會初始化函數內部的局部變量和命名參數例如add01函數里的a,b,把這些變量存儲到一個活動對象的變量里,初始化完成后這個活動對象也會加入到運行期上下文的作用域鏈里,這個活動對象會放到運行期上下文作用域鏈的最前端。其實在函數執行完畢銷毀的對象就是這個活動對象,活動對象被銷毀了對應的運行期上下文也就被銷毀,但是原來存儲在函數里的作用域還是保留的。
作用域鏈的作用是對變量標識符進行解析,標識符就是變量的名字,例如add01、add02這些就是標識符,標識符的解析就是獲取數據的位置或是如何存儲數據。當函數執行時候,遇到每一個變量就會搜索運行期上下文的作用域鏈,這個過程都是從作用域鏈的頭開始查找,也就是我上面說的從活動對象開始找起,如果找到與之對應的標識符,那么搜索過程便會停止,如果沒有找到那么接著就會搜索作用域鏈的下一個對象,直到找到為止。不管是函數的作用域鏈還是運行期上下文的作用域鏈,鏈條的最后一個都是全局對象,其實全局對象是一個特殊的對象,如果我還是套用前面函數作用域的方式去理解全局對象,把一個網頁當做一個最大的函數,這個函數對象就是window,網頁的打開時這個window函數生命周期的開始,網頁的關閉就是window函數的銷毀,與window函數對應的還有一個全局的運行期上下文,那么用上面的理論理解全局變量應該就會簡單多了。全局變量可以當做所有函數作用域的父作用域,當標識符解析到了全局變量時候,前面一定經過了n多個的作用域鏈中元素的遍歷,因此到了遍歷全局變量其實程序執行的效率是很低了,所以所有寫高性能javascript代碼的建議里都是要盡量減少全局變量的使用,如果非要使用全局變量也要在函數作用域內用一個局部變量替代全局變量,這是高性能javascript代碼的一個基本要求,不過時下最新版的瀏覽器幾乎都對這個特性進行了優化,訪問全局變量不再像以前那么消耗性能了,不過ie的老版本的市場還是很大,而老版本的ie對全局訪變量的訪問那就是代碼效率的毒瘤了。
下面我要講講閉包和作用域的關系,有很多人把閉包等價于作用域或者是作用域鏈,這個有一定的道理,但是如果真的以為閉包就是等價于的這些的話,這個等價于就是錯誤的,閉包在javascript語言里是一個特殊的函數,閉包產生于函數執行的時候,也就是函數的名字加上了小括號使用的時候,這個時候就會創建閉包或者叫做定義閉包,這個過程和函數創建作用域的過程一樣,而閉包的作用域鏈就是函數的運行期上下文,當執行閉包或者說使用閉包的時候,那么就會構建出閉包的運行期上下文,這個時候閉包也會構建一個活動對象,這個活動對象被置于它的作用域鏈的最前端,同時閉包還包含執行函數的活動對象,但這個活動變量是在閉包活動變量的后面,這樣就會導致函數銷毀時候內存無法及時回收,造成大量的內存資源的占用,因此使用閉包是個十分消耗計算資源的應用,前面我講到要盡量把全局變量用局部變量替換,其實碰到跨作用域的變量引用,都要將其用局部變量替代,這樣代碼的效率和安全性會更好。
還有很多童鞋認為this指針和作用域鏈沒什么聯系,這種理解是不正確的,其實this指針就是指向作用域鏈的上一級對象,但這種理解常常會誤導某些人對this的理解,因為很多人在函數內部調用函數時候,發現被調用的函數this指針指向了全局變量,我們在實際開發里使用this指針最好是換個角度,this指針是調用某個函數上一級對象,例如obj.ftn(),那么ftn函數內部的this指針就是指向的obj,如果有個函數直接是ftn(),前面沒有點,那么ftn函數里的內部指針就是window對象,this指針都是指向點號前面的對象,如果沒有點號就是window對象,通過點號理解this指針比較方便,也不容易出錯,但是通過作用域理解this為什么會有偏差了,原因就是全局作用域在作祟,在javascript里不管哪里直接調用函數,前面沒有點的時候this都是指向全局的,我們不應該看這個方法是放到哪個函數里執行,其實this和作用域關系是緊密的,大家千萬別懷疑這點。此外在作用域鏈的數組型的數據結構里,數組的每一個元素都含有一個this指針,this都是作為一個鍵值對預先定義好的,它的取值不由作用域鏈的創建所決定。因此有人認為this指針的使用其實也是跨域訪問的觀點也是不對,this指針的使用不存在跨域,它的效率也是非常高的。
理解了上面這個問題,那么我們對函數的apply和call方法深入理解就比較容易了,這兩個方法的本質作用就是在特定的作用域里調用函數,這個解釋比較抽象,這樣我們先看下面的例子:
function test(){ console.log(this); } test.call(window);// window var obj = {}; test.call(obj);// object function ftn01(){ test.call(this); } ftn01();// window
這兩個方法的第一個參數就是改變當前活動對象里this指針指向哪個對象,第一個參數就是函數this指針指向的對象,其本質也是改變作用域的一種方式,改變作用域的方法可以擴展方法的使用范圍,因此調用對象和方法解耦,這樣可以精簡代碼,提高代碼的復用程度。
盡量少使用全局變量,多用局部變量是寫高效javascript程序的一個基本要求,大家不要懷疑它會過時,不管瀏覽器如何演進,這點規則都是一條黃金規則。記住最優秀的javascript代碼里全局變量最好只有一個。
最后我還要插一句,做過javascript都知道javascript代碼壓縮是一個提升網站效率的重要手段,我們使用雅虎或者谷歌的壓縮代碼的工具時候,會發現很多變量都會被A,B這樣簡單的代碼所替換,這個替換規則都是對局部變量進行的,因此多使用局部變量會javascript壓縮效果更好。
文章列表