JS-------作用域和閉包

一、大家可以先看一個例子

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();  
美女()  //"美女的名字"

這下,美女是一個閉包了,單身汪想怎么玩就怎么玩了。(但并不推薦單身汪用中文做變量名的寫法,大家不要學(xué))。
這樣的例子該如何理解,我們就需要討論一下作用域了。

二、變量的作用域

變量的作用域指的是,變量起作用的范圍。也就是能訪問到變量的有效范圍。

JavaScript的變量依據(jù)作用域的范圍可以分為:

  • 全局變量
  • 局部變量

2.1 全局變量

==定義在函數(shù)外部的變量都是全局變量。==

全局變量的作用域是==當(dāng)前文檔==,也就是當(dāng)前文檔所有的JavaScript腳本都可以訪問到這個變量。

下面的代碼是書寫在同一個HTML文檔中的2個JavaScript腳本:

<script type="text/javascript">
    //定義了一個全局變量。那么這個變量在當(dāng)前html頁面的任何的JS腳本部分都可以訪問到。
    var v = 20; 
    alert(v); //彈出:20
</script>
<script type="text/javascript">
    //因為v是全局變量,所以這里仍然可以訪問到。
    alert(v);  //彈出:20
</script>

再看下面一段代碼 :

<script type="text/javascript">
    alert(a);
    var a = 20;
</script>

運行這段代碼并不會報錯, alert(a); 這行代碼彈出:undefined。

為什么在聲明 a 之前可以訪問變量 a 呢? 能訪問 a 為什么輸出是undefined而不是20呢?

==聲明提前!==

  • 所有的全局變量的聲明都會提前到JavaScript的前端聲明。也就是所有的全局變量都是先聲明的,并且早于其他一切代碼。
  • 但是變量的賦值的位置并不會變,仍然在原位置賦值。

所以上面的代碼等效下面的代碼:

<script type="text/javascript">
    var a; //聲明提前
    alert(a);
    a = 20; //賦值仍然在原來的位置
</script>

2.2 局部變量

在函數(shù)內(nèi)聲明的變量,叫局部變量!表示形參的變量也是局部變量!

局部變量的作用域是局部變量所在的整個函數(shù)的內(nèi)部。 在函數(shù)的外部不能訪問局部變量。

<script type="text/javascript">
    function f(){
        alert(v);  //   彈出:undefined
        var v = "abc";  // 聲明局部變量。局部變量也會聲明提前到函數(shù)的最頂端。
        alert(v);   //  彈出:abc
    }
    alert(v);  //報錯。因為變量v沒有定義。 方法 f 的外部是不能訪問方法內(nèi)部的局部變量 v 的。
 </script>

2.3 全局變量和局部變量的一些細節(jié)

看下面一段代碼:

<script type="text/javascript">
    var m = 10;
    function f(){
        var m = 20;
        alert("方法內(nèi)部:" + m);  //代碼1
    }
    f();
    alert("方法外部:" + m); //代碼2
</script>

在方法內(nèi)部訪問m,訪問到的是哪個m呢?局部變量的m還是全局變量的m?

2.3.1 全局變量和局部變量重名問題

  1. 在上面的代碼中,當(dāng)局部變量與全局變量重名時,局部變量的作用域會覆蓋全局變量的作用域。也就是說在函數(shù)內(nèi)部訪問重名變量時,訪問的是局部變量。==所以 "代碼1" 部分輸出的是20。==
  2. 當(dāng)函數(shù)返回離開局部變量的作用域后,又回到全局變量的作用域。==所以代碼2輸出10。==
  3. 如何在函數(shù)訪問同名的全局變量呢?==通過:window.全局變量名==
<script type="text/javascript">
    var m = 10;
    function f(){
        var m = 20;
        alert(window.m);  //訪問同名的全局變量。其實這個時候相當(dāng)于在訪問window這個對象的屬性。
    }
    f();  
</script>

2.3.2 JavaScript中有沒有塊級作用域?

看下面一段代碼:

<script type="text/javascript">
  var m = 5;
  if(m == 5){
    var n = 10;
  }
  alert(n); //代碼1
</script>

代碼1輸出什么? undefined還是10?還是報錯?

==輸出10!==

  • JavaScript的作用域是按照函數(shù)來劃分的
  • ==JavaScript沒有塊級作用域==

在上面的代碼中,變量 n 雖然是在 if 語句內(nèi)聲明的,但是它仍然是全局變量,而不是局部變量。

只有定義在方法內(nèi)部的變量才是局部變量

注意:

  • 即使我們把變量的聲明放在 if、for等塊級語句內(nèi),也會進行==聲明提前==的操作!

三、作用域鏈---作用域的深入理解

3.1 執(zhí)行環(huán)境

? 執(zhí)行環(huán)境( execution context )是 JavaScript 中最為重要的一個概念。執(zhí)行環(huán)境定義了變量或函數(shù)有權(quán)訪問的其他數(shù)據(jù),決定了它們各自的行為。每個執(zhí)行環(huán)境都有一個與之關(guān)聯(lián)的 變量對象(variable object),環(huán)境中定義的所有變量和函數(shù)都保存在這個對象中。雖然我們編寫的代碼無法訪問這個對象,但解析器在處理數(shù)據(jù)時會在后臺使用它。

? 全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境。在 Web 瀏覽器中,全局執(zhí)行環(huán)境被認為是 window 對象,因此所有全局變量和函數(shù)都是作為 window 對象的屬性和方法創(chuàng)建的。對全局執(zhí)行環(huán)境變量來說,變量對象就是window對象,對函數(shù)來說,變量對象就是這個函數(shù)的 活動對象 ,活動對象是在函數(shù)調(diào)用時創(chuàng)建的一個內(nèi)部變量。

? 每個函數(shù)都有自己的執(zhí)行環(huán)境,當(dāng)執(zhí)行流進入一個函數(shù)時,函數(shù)的執(zhí)行環(huán)境就會被推入一個執(zhí)行環(huán)境棧中。而在函數(shù)執(zhí)行之后,棧將執(zhí)行結(jié)束的函數(shù)的執(zhí)行環(huán)境彈出,把控制權(quán)返回給之前的執(zhí)行環(huán)境。

3.2 作用域鏈

? 作用域鏈與一個執(zhí)行環(huán)境相關(guān),作用域鏈用于在標示符解析中變量查找。

? 在JavaScript中,函數(shù)也是對象,實際上,JavaScript里一切都是對象。函數(shù)對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內(nèi)部屬性。其中一個內(nèi)部屬性是[[Scope]],由ECMA-262標準第三版定義,他就指向了這個函數(shù)的作用域鏈。作用域鏈中存儲的是與每個執(zhí)行環(huán)境相關(guān) **變量對象 **(函數(shù)內(nèi)部也是活動對象)。

? 當(dāng)創(chuàng)建一個函數(shù)( 聲明一個函數(shù) )后,那么會創(chuàng)建這個函數(shù)的作用域鏈。這個函數(shù)的作用域鏈在這個時候只包含一個變量對象(window)

<script type="text/javascript">
    function sum(num1, num2){
        var sum = num1 + num2;
        return sum;
    }
</script>

函數(shù) sum 的作用域鏈示意圖:

說明:

  • 函數(shù)創(chuàng)建的時候,這個時候作用域鏈中只有一個 變量對象 (window)

當(dāng)執(zhí)行下面的代碼:

<script type="text/javascript">
    function sum(num1, num2){
        var sum = num1 + num2;
        return sum;
    }
    var sum = sum(3, 4);
</script>
當(dāng)調(diào)用 sum 函數(shù)時,會首先創(chuàng)建一個 **“執(zhí)行環(huán)境”**,這個 **執(zhí)行環(huán)境** 有自己的作用域鏈,這個作用域鏈初始化為 sum 函數(shù)的 [[scope]] 所包含的對象。然后創(chuàng)建一個 與這個執(zhí)行環(huán)境相關(guān)的 **變量對象( 活動對象 )** ,這個 **變量對象** 中存儲了在這個函數(shù)中定義的所有參數(shù)、變量和函數(shù)。把 **變量對象** 存儲在作用域中的頂端。  以后在查找變量的時候,總是從作用域鏈條的頂端開始查找,一直到作用域鏈條的末端。

看下面的示意圖:

說明:

  1. 在sum中訪問一個變量的時候,總是從作用域鏈的頂端開始查找,如果找到就得到結(jié)果,如果找到不到就一直查找,直到作用域鏈的末端。
  2. 因為在方法內(nèi)的存在變量和函數(shù)的聲明提前現(xiàn)象,所以函數(shù)一旦執(zhí)行 函數(shù)的活動對象(變量對象)中總是保存了這個韓碩中聲明的所有變量和函數(shù)。
  3. 如果在函數(shù)中又定義了一個內(nèi)部函數(shù)(還沒有執(zhí)行),則這個時候內(nèi)部函數(shù)的作用域,是包含了外部函數(shù)的作用域。 一旦內(nèi)部函數(shù)開始執(zhí)行則把自己的活動對象添加到了這個作用域的頂端。
<script type="text/javascript">
    function sum(num1, num2){
        var sum = num1 + num2;
        function inner (a) {
            
        }

        return sum;
    }

    var sum = sum(3, 4);
</script>

內(nèi)部函數(shù)的作用域:

函數(shù)執(zhí)行后的作用域示意圖不再畫出。

四、閉包

看下面的代碼:

<script type="text/javascript">
    function createSumFunction(num1, num2){
        return function () {
            return num1 + num2;
        };
    }

    var sumFun = createSumFunction(3, 4);
    var sum = sumFun();
    alert(sum);
</script>

? 在上面的代碼中,createSumFunction函數(shù)返回了一個匿名函數(shù),而這個匿名函數(shù)使用了createSumFunction函數(shù)中的局部變量(參數(shù)),即使createSumFunction這個函數(shù)執(zhí)行結(jié)束了,由于作用域鏈的存在,他的局部變量在匿名函數(shù)中仍然可以使用,這個匿名函數(shù)就是閉包。

? 閉包是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。

? 閉包是一種特殊的對象。它由兩部分構(gòu)成: 函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境 。環(huán)境由閉包創(chuàng)建時在作用域中的任何局部變量組成。在我們的例子中,sumFun 是一個閉包,由 匿名 函數(shù)和閉包創(chuàng)建時存在的num1num2 兩個局部變量組成。

五、閉包的應(yīng)用

5.1 返回外部函數(shù)的局部變量

<script type="text/javascript">
    function outer () {
        var num = 5;
        //定義一個內(nèi)部函數(shù)
        function inner () {
            //內(nèi)部函數(shù)的返回值是外部函數(shù)的一個局部變量
            return num;
        }
        //把局部變量的值++
        num++;
        // 返回內(nèi)部函數(shù)
        return inner;
    }
    var num = outer()();  // 6
    alert(num);  
</script>

說明:

  1. 這例子中,雖然函數(shù)的聲明在num++之前,但是函數(shù)返回的時候num已經(jīng)++過了,所以只是num自增之后的值。
  2. 結(jié)論:閉包中使用的局部變量的值,一定是局部變量的最后的值。

5.2 使用函數(shù)自執(zhí)行和閉包封裝對象

封裝一個能夠增刪改查的對象

<script type="text/javascript">
    var person = (function () {
        //聲明一個對象,增刪改查均是針對這個對象
        var personInfo = {
            name : "李四",
            age : 20
        };
        //返回一個對象,這個對象中封裝了一些對personInfor操作的方法
        return {
            //根據(jù)給定的屬性獲取這個屬性的值
            getInfo : function (property) {
                return personInfo[property];
            },
            //修改一個屬性值
            modifyInfo : function (property, newValue) {
                personInfo[property] = newValue;
                
            },
            //添加新的屬性
            addInfo : function (property, value) {
                personInfo[property] = value;
                
            },
             //刪除指定的屬性
            delInfo : function (property) {
                delete personInfo[property];
                
            }
        }
    })();
    alert(person.getInfo("name"));
    person.addInfo("sex", "男");
    alert(person.getInfo("sex"));
</script>

5.3 for循環(huán)典型問題

看下面的代碼

<body>
    <input type="button" value="按鈕1"    >
    <input type="button" value="按鈕2"    >
    <input type="button" value="按鈕3"    >
    <script type="text/javascript">
        var btns = document.getElementsByTagName("input");
        for (var i = 0; i < 3; i++) {
            btns[i].onclick = function () {
                alert("我是第" + (i + 1) + "個按鈕");
            };
        }
    </script>
</body> 

發(fā)現(xiàn)在點擊三個按鈕的時候都是彈出 我是第4個按鈕。 為什么呢?閉包導(dǎo)致的! 每循環(huán)一次都會有一個匿名函數(shù)設(shè)置點擊事件,閉包總是保持的變量的最后一個值,所以點擊的時候,總是讀的是 i 的組后一個值4.

解決方案1:給每個按鈕添加一個屬性,來保存 每次 i 的臨時值

<body>
    <input type="button" value="按鈕1"    >
    <input type="button" value="按鈕2"    >
    <input type="button" value="按鈕3"    >
    <script type="text/javascript">
        var btns = document.getElementsByTagName("input");
        for (var i = 0; i < 3; i++) {
            //把i的值綁定到按鈕的一個屬性上,那么以后i的值就和index的值沒有關(guān)系了。
            btns[i].index = i;
            btns[i].onclick = function () {
                alert("我是第" + (this.index + 1) + "個按鈕");
            };
        }
    </script>
</body>

解決方案2:使用匿名函數(shù)的自執(zhí)行

<body>
    <input type="button" value="按鈕1"    >
    <input type="button" value="按鈕2"    >
    <input type="button" value="按鈕3"    >
    <script type="text/javascript">
        var btns = document.getElementsByTagName("input");
        for (var i = 0; i < 3; i++) {   
            //因為匿名函數(shù)已經(jīng)執(zhí)行了,所以會把 i 的值傳入到num中,注意是i的值,所以num
            (function (num) {
                btns[i].onclick = function () {
                    alert("我是第" + (num + 1) + "個按鈕");
                }
            })(i);
        }
    </script>
</body>

相信通過上面的例子說明,大家對什么是作用域和閉包已經(jīng)有了更深層次的了解。那么大家來看看下面的2個例子打印的結(jié)果是什么?

var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      return function(){
        return this.name;
      };
    }
  };
  alert(object.getNameFunc()());
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容