做用域&做用域鏈和with,catch語句&閉包


做用域(函數)

做用域:變量與函數的可訪問範圍,即做用域控制着變量與函數的可見性和生命週期;
在一些類C編程語言中花括號內的每一段代碼都有各自的做用域,並且變量在聲明它們的代碼段外是不可見的,稱之爲塊級的做用域;JavaScript容易讓初學者誤會的地方也在於此,JavaScript並無塊及的做用域,只有函數級做用域:變量在聲明它們的函數體及其子函數內是可見的。
變量的做用域:全局做用域和局部做用域兩種.javascript

全局變量(global scope):做用域是全局,在代碼的任何地方都有定義;
局部變量(local scope):做用域函數局部,函數參數和局部變量只在函數體內定義;前端

局部變量的優先級 高於同名的全局變量;
聲明局部變量時,必定要使用var ,不然解釋器會將變量當作全局對象window屬性;java

window對象的全部屬性擁有全局做用域;在代碼任何地方均可以訪問,函數內部聲明而且以var修飾的變量就是局部變量,只能在函數體內使用,函數的參數雖然沒有使用var但仍然是局部變量。編程

var test1 = "globalVariable";
  function test(){
    console.log(test1); //globalVariable 
    test1 = "localVariable";//去掉  var
    console.log(test1); //localVariable
  }
 test();

JavaScript變量函數的解析或者聲明提早,JavaScript雖然是解釋執行,但也不是循序漸進逐句解釋執行的,在真正解釋執行以前,JavaScript解釋器會預解析代碼,將變量、函數聲明部分提早解釋,這就意味着咱們能夠在function聲明語句以前調用function,可是對於變量的不能夠;閉包

foo();//1
function foo() {
   return console.log(1);
  //return console.log(foo);==>function foo() {return console.log( foo);};
  //return console.log(typeof foo); ==> function;
}

console.log(a); //undefined
var a=3;
console.log(a); //3
console.log(b); //Uncaught ReferenceError: b is not defined

編輯器執行效果:
var a;
console.log(a); //undefined
a=3;
console.log(a); //3
函數內的變量聲明提早:
var test1 = "globalVariable";
  function test(){
    console.log(test1); //undefined 
    var test1 = "localVariable";
    console.log(test1); //localVariable
  }
 test();

編輯器執行效果:
 var test1 = "globalVariable";
 function test(){
    var test1;  //將函數內的變量聲明提早至函數頂部
    console.log(test1);  //undefined 
    test1 = "localVariable";  //賦值
   console.log(test1);  //localVariable
 }
情景1
function test(){  
    for(var i=0;i<3;i++){  
        console.log(i); // 輸出 0 1 2   
    }  
    console.log(i); // 輸出 3  
}  
test();
情景2
  function test(){  
            var  i=0;
             return  function(){
                 console.log(i++);
              //   console.log(++i);
            }  
        }
       t2=test();
      var t1=test();

      t1();//0    
      t1();//1
      t2();//0
      t2();//1
      t2();//2


情景一:
var test1 ;
function test(){
   test1 = "localVariable";
}
console.log(test1); //undefined 
 test();
 console.log(test1)//localVariable

情景二:
var test1 ;
function test(){
  var test1 = "localVariable";
}
console.log(test1); //undefined;
 test();
 console.log(test1)//undefined;

is not defined:未定義;
undefined:定義了,未賦值de類型是undefined;
typeof  "undefined"  //"string"
typeof  undefined   //undefined;
Number(undefined)  //NaN;

Null:未存在;
typeof Null //object;
NaN: 非空;
typeof NaN  //number;
Number(null) // 0;

做用域鏈

執行此函數時會建立一個稱爲「運行期上下文(execution context)」的內部對象,運行期上下文定義了函數執行時的環境。每一個運行期上下文都有本身的做用域鏈,用於標識符解析,當運行期上下文被建立時,而它的做用域鏈初始化爲當前運行函數的[[Scope]]所包含的對象。編程語言

  這些值按照它們出如今函數中的順序被複制到運行期上下文的做用域鏈中。它們共同組成了一個新的對象,叫「活動對象(activation object)」,該對象包含了函數的全部局部變量、命名參數、參數集合以及this,而後此對象會被推入做用域鏈的前端,當運行期上下文被銷燬,活動對象也隨之銷燬。
  在函數執行過程當中,沒遇到一個變量,都會經歷一次標識符解析過程以決定從哪裏獲取和存儲數據。該過程從做用域鏈頭部,也就是從活動對象開始搜索,查找同名的標識符,若是找到了就使用這個標識符對應的變量,若是沒找到繼續搜索做用域鏈中的下一個對象,若是搜索完全部對象都未找到,則認爲該標識符未定義。函數執行過程當中,每一個標識符都要經歷這樣的搜索過程。編輯器

從做用域鏈的結構能夠看出,在運行期上下文的做用域鏈中,標識符所在的位置越深,讀寫速度就會越慢。如上圖所示,由於全局變量老是存在於運行期上下文做用域鏈的最末端,所以在標識符解析的時候,查找全局變量是最慢的。因此,在編寫代碼的時候應儘可能少使用全局變量,儘量使用局部變量。一個好的經驗法則是:若是一個跨做用域的對象被引用了一次以上,則先把它存儲到局部變量裏再使用。函數

function changeColor(){
    document.getElementById("btnChange").onclick=function(){
        document.getElementById("targetCanvas").style.backgroundColor="red";
    };
}
這個函數引用了兩次全局變量document,查找該變量必須遍歷整個做用域鏈,直到最後在全局對象中才能找到。這段代碼能夠重寫以下:

function changeColor(){
    var doc=document;
    doc.getElementById("btnChange").onclick=function(){
        doc.getElementById("targetCanvas").style.backgroundColor="red";
    };
}

這段代碼比較簡單,重寫後不會顯示出巨大的性能提高,可是若是程序中有大量的全局變量被從反覆訪問,那麼重寫後的代碼性能會有顯著改善。

with語句

做用域鏈只會被 with 語句和 catch 語句影響。
 函數每次執行時對應的運行期上下文都是獨一無二的,因此屢次調用同一個函數就會致使建立多個運行期上下文,當函數執行完畢,執行上下文會被銷燬。每個運行期上下文都和一個做用域鏈關聯。通常狀況下,在運行期上下文運行的過程當中,其做用域鏈只會被 with 語句和 catch 語句影響。性能

with語句是對象的快捷應用方式,用來避免書寫重複代碼。優化

function initUI(){
    with(document){
        var bd=body,
            links=getElementsByTagName("a"),
            i=0,
            len=links.length;
        while(i < len){
            update(links[i++]);
        }
        getElementById("btnInit").onclick=function(){
            doSomething();
        };
    }
}
這裏使用width語句來避免屢次書寫document,看上去更高效,實際上產生了性能問題。
當代碼運行到with語句時,運行期上下文的做用域鏈臨時被改變了。一個新的可變對象被建立,它包含了參數指定的對象的全部屬性。這個對象將被推入做用域鏈的頭部,這意味着函數的全部局部變量如今處於第二個做用域鏈對象中,所以訪問代價更高了。



所以在程序中應避免使用with語句,在這個例子中,只要簡單的把document存儲在一個局部變量中就能夠提高性能。

  另一個會改變做用域鏈的是try-catch語句中的catch語句。當try代碼塊中發生錯誤時,執行過程會跳轉到catch語句,而後把異常對象推入一個可變對象並置於做用域的頭部。在catch代碼塊內部,函數的全部局部變量將會被放在第二個做用域鏈對象中。示例代碼:
try{
    doSomething();
}catch(ex){
    alert(ex.message); //做用域鏈在此處改變
}
請注意,一旦catch語句執行完畢,做用域鏈機會返回到以前的狀態。try-catch語句在代碼調試和異常處理中很是有用,所以不建議徹底避免。你能夠經過優化代碼來減小catch語句對性能的影響。一個很好的模式是將錯誤委託給一個函數處理,例如
try{
    doSomething();
}catch(ex){
    handleError(ex); //委託給處理器方法
}

優化後的代碼,handleError方法是catch子句中惟一執行的代碼。該函數接收異常對象做爲參數,這樣你能夠更加靈活和統一的處理錯誤。因爲只執行一條語句,且沒有局部變量的訪問,做用域鏈的臨時改變就不會影響代碼性能了。

閉包

如何從外部讀取局部變量?

 function f1(){
    var n=999;
    function f2(){
      alert(n); 
    }
    return f2;
  }
  var result=f1();
  result(); // 999

f2函數,就是閉包,閉包就是可以讀取其餘函數內部變量的函數。因爲在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。
因此,在本質上,閉包就是將函數內部和函數外部鏈接起來的一座橋樑。

閉包的用途;

閉包能夠用在許多地方。它的最大用處有兩個,一個是前面提到的能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中。


  function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

result實際上就是閉包f2函數。它一共運行了兩次,第一次的值是999,第二次的值是1000。這證實了,函數f1中的局部變量n一直保存在內存中,並無在f1調用後被自動清除。


爲何會這樣呢?緣由就在於f1是f2的父函數,而f2被賦給了一個全局變量,這致使f2始終在內存中,而f2的存在依賴於f1,所以f1也始終在內存中,不會在調用結束後,被垃圾回收機制(garbage collection)回收。
這段代碼中另外一個值得注意的地方,就是"nAdd=function(){n+=1}"這一行,首先在nAdd前面沒有使用var關鍵字,所以nAdd是一個全局變量,而不是局部變量。其次,nAdd的值是一個匿名函數(anonymous function),而這個匿名函數自己也是一個閉包,因此nAdd至關因而一個setter,能夠在函數外部對函數內部的局部變量進行操做。
5、使用閉包的注意點
1)因爲閉包會使得函數中的變量都被保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄露。解決方法是,在退出函數以前,將不使用的局部變量所有刪除。
2)閉包會在父函數外部,改變父函數內部變量的值。因此,若是你把父函數看成對象(object)使用,把閉包看成它的公用方法(Public Method),把內部變量看成它的私有屬性(private value),這時必定要當心,不要隨便改變父函數內部變量的值。
相關文章
相關標籤/搜索