瞭解 JS 做用域與做用域鏈

(1)做用域javascript

一個變量的做用域(scope)是程序源代碼中定義的這個變量的區域。html

1. 在JS中使用的是詞法做用域(lexical scope)java

不在任何函數內聲明的變量(函數內省略var的也算全局)稱做全局變量(global scope)c++

在函數內聲明的變量具備函數做用域(function scope),屬於局部變量面試

局部變量優先級高於全局變量閉包

var name="one";
function test(){
  var name="two";
  console.log(name); //two
}
test();

函數內省略var的,會影響全局變量,由於它實際上已經被重寫成了全局變量函數

var name="one";
function test(){
  name="two";
  
}
test();
console.log(name); //two

函數做用域,就是說函數是一個做用域的基本單位,js不像c/c++那樣具備塊級做用域 好比 if  for 等spa

function test(){
  for(var i=0;i<10;i++){
    if(i==5){
      var name = "one";
    }
  }
  console.log(name); //one
}

test();  //由於是函數級做用域,因此能夠訪問到name="one"

固然了,js裏邊還使用到了高階函數,其實能夠理解成嵌套函數code

function test1(){
  var name = "one";
  return function (){
    console.log(name);
  }
}
test1()();

test1()以後將調用外層函數,返回了一個內層函數,再繼續(),就相應調用執行了內層函數,因此就輸出 」one"orm

嵌套函數涉及到了閉包,後面再談..這裏內層函數能夠訪問到外層函數中聲明的變量name,這就涉及到了做用域鏈機制


2. JS中的聲明提早

js中的函數做用域是指在函數內聲明的全部變量在函數體內始終是可見的。而且,變量在聲明以前就可使用了,這種狀況就叫作聲明提早(hoisting)

tip:聲明提早是在js引擎預編譯時就進行了,在代碼被執行以前已經有聲明提早的現象產生了

好比

var name="one";
function test(){
  console.log(name);  //undefined
  var name="two";
  console.log(name); //two
}

test();

上邊就達到了下面的效果

var name="one";
function test(){
  var name;
  console.log(name);  //undefined
  name="two";
  console.log(name); //two
}

test();

再試試把var去掉?這是函數內的name已經變成了全局變量,因此再也不是undefined

var name="one";
function test(){
  console.log(name);  //one
  name="two";
  console.log(name); //two
}

test();

3. 值得注意的是,上面提到的都沒有傳參數,若是test有參數,又如何呢?

function test(name){
  console.log(name);  //one
  name="two";
  console.log(name); //two
}

var name = "one";
test(name);
console.log(name); // one

以前說過,基本類型是按值傳遞的,因此傳進test裏面的name實際上只是一個副本,函數返回以後這個副本就被清除了。
千萬不要覺得函數裏邊的name="two"把全局name修改了,由於它們是兩個獨立的name

 

(2)做用域鏈

上面提到的高級函數就涉及到了做用域鏈

function test1(){
  var name = "one";
  return function (){
    console.log(name);
  }
}
test1()();

1. 引入一大段話來解釋:
每一段js代碼(全局代碼或函數)都有一個與之關聯的做用域鏈(scope chain)。

這個做用域鏈是一個對象列表或者鏈表,這組對象定義了這段代碼中「做用域中」的變量。

當js須要查找變量x的值的時候(這個過程稱爲變量解析(variable resolution)),它會從鏈的第一個對象開始查找,若是這個對象有一個名爲x的屬性,則會直接使用這個屬性的值,若是第一個對象中沒有名爲x的屬性,js會繼續查找鏈上的下一個對象。若是第二個對象依然沒有名爲x的屬性,則會繼續查找下一個,以此類推。若是做用域鏈上沒有任何一個對象含有屬性x,那麼就認爲這段代碼的做用域鏈上不存在x,並最終拋出一個引用錯誤(ReferenceError)異常。

2. 做用域鏈舉例:

在js最頂層代碼中(也就是不包括任何函數定義內的代碼),做用域鏈由一個全局對象組成。

在不包含嵌套的函數體內,做用域鏈上有兩個對象,第一個是定義函數參數和局部變量的對象,第二個是全局對象。

在一個嵌套的函數體內,做用域上至少有三個對象。

3. 做用域鏈建立規則:

當定義一個函數時(注意,是定義的時候就開始了),它實際上保存一個做用域鏈。

當調用這個函數時,它建立一個新的對象來儲存它的參數或局部變量,並將這個對象添加保存至那個做用域鏈上,同時建立一個新的更長的表示函數調用做用域的「鏈」。

對於嵌套函數來講,狀況又有所變化:每次調用外部函數的時候,內部函數又會從新定義一遍。由於每次調用外部函數的時候,做用域鏈都是不一樣的。內部函數在每次定義的時候都要微妙的差異---在每次調用外部函數時,內部函數的代碼都是相同的,並且關聯這段代碼的做用域鏈也不相同。

 (tip: 把上面三點理解好,記住了,最好還要能用本身的話說出來,否則就背下來,由於面試官就直接問你:請描述一下做用域鏈...)

 

舉個做用域鏈的實用例子:

var name="one";
function test(){
  var name="two";
  function test1(){
    var name="three";
    console.log(name);  //three
  }
  function test2(){
    console.log(name);  // two
  }
  
  test1();
  test2();
}

test();

上邊是個嵌套函數,相應的應該是做用域鏈上有三個對象
那麼在調用的時候,須要查找name的值,就在做用域鏈上查找

當成功調用test1()的時候,順序爲 test1()->test()->全局對象window 由於在test1()上就找到了name的值three,因此完成搜索返回

當成功調用test1()的時候,順序爲 test2()->test()->全局對象window  由於在test2()上沒找到name的值,因此找test()中的,找到了name的值two,就完成搜索返回

還有一個例子有時候咱們會犯錯的,面試的時候也常常被騙到。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript">
function buttonInit(){
    for(var i=1;i<4;i++){
        var b=document.getElementById("button"+i);
        b.addEventListener("click",function(){ 
            alert("Button"+i); //都是 Button4
        },false);
    }
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>

爲何?
根據做用域鏈中變量的尋找規則:

b.addEventListener("click",function(){ 
            alert("Button"+i);
        },false);

這裏有一個函數,它是匿名函數,既然是函數,那就在做用域鏈上具備一個對象,這個函數裏邊使用到了變量i,它天然會在做用域上尋找它。
查找順序是 這個匿名函數 -->外部的函數buttonInit() -->全局對象window

匿名函數中找不到i,天然跑到了buttonInit(), ok,在for中找到了,

這時註冊事件已經結束了,不要覺得它會一個一個把i放下來,由於函數做用域以內的變量對做用域內是一直可見的,就是說會保持到最後的狀態

當匿名函數要使用i的時候,註冊事件完了,i已經變成了4,因此都是Button4

那怎麼解決呢?

給它傳值進去吧,每次循環時,再使用一個匿名函數,把for裏邊的i傳進去,匿名函數的規則如代碼

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script type="text/javascript">
function buttonInit(){
    for(var i=1;i<4;i++){
        (function(data_i){
        var b=document.getElementById("button"+data_i);
        b.addEventListener("click",function(){ 
            alert("Button"+data_i);
        },false);
        })(i);
    }
}
window.onload=buttonInit;
</script>
</head>
<body>
<button id="button1">Button1</button>
<button id="button2">Button2</button>
<button id="button3">Button3</button>
</body>
</html>

這樣就能夠 Button1..2..3了

4.上述就是做用域鏈的基本描述,另外,with語句可用於臨時拓展做用域鏈(不推薦使用with)

語法形如:

with(object)

statement

這個with語句,將object添加到做用域鏈的頭部,而後執行statement,最後把做用域鏈恢復到原始狀態

簡單用法:

好比給表單中各個項的值value賦值

通常能夠咱們直接這樣

var f = document.forms[0];
f.name.value = "";
f.age.value = "";
f.email.value = "";

引入with後(由於使用with會產生一系列問題,因此仍是使用上面那張形式吧)

with(document.forms[0]){
f.name.value = "";
f.age.value = "";
f.email.value = "";
}

另外,假如 一個對象o具備x屬性,o.x = 1;
那麼使用

with(o){
  x = 2;
}

就能夠轉換成 o.x = 2;
假如o沒有定義屬性x,它的功能就只是至關於  x = 2; 一個全局變量罷了。

由於with提供了一種讀取o的屬性的快捷方式,但他並不能建立o自己沒有的屬性。

相關文章
相關標籤/搜索