JavaScript中作用域鏈和閉包總結

一、匿名函數

1.1 匿名函數的概念

​ 聲明一個沒有函數名的函數,就是匿名函數。

​ 有函數名的函數就是具名函數。

看下面的代碼:

<script type="text/javascript">
    /*
    //這裏定義了一個函數,而且沒有函數名。這樣寫語法是錯誤的,如果允許這樣定義,那麼根本就沒有辦法調用。
    //所以,我們可以用一個變量來存儲一下
    function(){ 

    }
    */
  // 聲明瞭一個匿名函數,並把匿名函數賦值給變量f。 注意這個時候這個匿名函數並沒有執行。
  var f = function(){
    alert("哥們我是匿名函數內的代碼");
  }
  //我們可以把變量 f 當做一個函數名來調用
  f();  //調用上面定義的匿名函數
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

1.2 匿名函數的應用場景

​ 有些場景大家已經比較熟悉了。

1.2.1 給標籤綁定事件

<script type="text/javascript">
    var btn = document.getElementById("btn");
    btn.onclick = function () {
        alert("點我幹嗎");
    }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1.2.2 在定時器中使用

<body>
    <h1></h1>
    <script type="text/javascript">
        var showTimeArar = document.getElementsByTagName("h1")[0];
        setInterval(function () {
            showTimeArar.innerHTML = new Date().toLocaleString();
        }, 1000);
    </script>
</body>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

1.2.3 給對象定義方法

<script type="text/javascript">
    var person = {
        name : "鳳姐",
        age : 30,
        play : function () {
            alert(this.name + "在美國玩");
        }
    }
    person.play();
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1.3 匿名函數的自調用

有些場景,我們需要定義完函數之後立即執行,這個時候可以定義一個匿名函數來完成。

(function () {
    alert("匿名函數立即執行")
})();
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

說明

  1. 需要把匿名函數用一對圓括號括起來,把匿名函數作爲一個整體來對待
  2. 最後再添加一對圓括號表示調用函數。這樣定義的匿名函數就會立即執行
  3. 當然,這個時候即使給這個函數加上方法名,也可以調用。不過這種情況爲什麼還要加方法名呢?

二、變量的作用域

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

JavaScript的變量依據作用域的範圍可以分爲:

  • 全局變量
  • 局部變量

2.1 全局變量

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

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

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

<script type="text/javascript">
    //定義了一個全局變量。那麼這個變量在當前html頁面的任何的JS腳本部分都可以訪問到。
    var v = 20; 
    alert(v); //彈出:20
</script>
<script type="text/javascript">
    //因爲v是全局變量,所以這裏仍然可以訪問到。
    alert(v);  //彈出:20
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

再看下面一段代碼 :

<script type="text/javascript">
    alert(a);
    var a = 20;
</script>
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

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

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

==聲明提前!==

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

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

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

2.2 局部變量

在函數內聲明的變量,叫局部變量!表示形參的變量也是局部變量!

局部變量的作用域是局部變量所在的整個函數的內部。 在函數的外部不能訪問局部變量。

<script type="text/javascript">
    function f(){
        alert(v);  //   彈出:undefined
        var v = "abc";  // 聲明局部變量。局部變量也會聲明提前到函數的最頂端。
        alert(v);   //  彈出:abc
    }
    alert(v);  //報錯。因爲變量v沒有定義。 方法 f 的外部是不能訪問方法內部的局部變量 v 的。
 </script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.3 全局變量和局部變量的一些細節

看下面一段代碼:

<script type="text/javascript">
    var m = 10;
    function f(){
        var m = 20;
        alert("方法內部:" + m);  //代碼1
    }
    f();
    alert("方法外部:" + m); //代碼2
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

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

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

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

看下面一段代碼:

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

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

==輸出10!==

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

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

只有定義在方法內部的變量纔是局部變量

注意:

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

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

3.1 執行環境

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

​ 全局執行環境是最外圍的一個執行環境。在 Web 瀏覽器中,全局執行環境被認爲是 window 對象,因此所有全局變量和函數都是作爲 window 對象的屬性和方法創建的。對全局執行環境變量來說,變量對象就是window對象,對函數來說,變量對象就是這個函數的 活動對象 ,活動對象是在函數調用時創建的一個內部變量。

​ 每個函數都有自己的執行環境,當執行流進入一個函數時,函數的執行環境就會被推入一個執行環境棧中。而在函數執行之後,棧將執行結束的函數的執行環境彈出,把控制權返回給之前的執行環境。

3.2 作用域鏈

​ 作用域鏈與一個執行環境相關,作用域鏈用於在標示符解析中變量查找。

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

​ 當創建一個函數( 聲明一個函數 )後,那麼會創建這個函數的作用域鏈。這個函數的作用域鏈在這個時候只包含一個變量對象(window)

<script type="text/javascript">
    function sum(num1, num2){
        var sum = num1 + num2;
        return sum;
    }
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函數 sum 的作用域鏈示意圖:

說明:

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

當執行下面的代碼:

<script type="text/javascript">
    function sum(num1, num2){
        var sum = num1 + num2;
        return sum;
    }
    var sum = sum(3, 4);
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
當調用 sum 函數時,會首先創建一個 **“執行環境”**,這個 **執行環境** 有自己的作用域鏈,這個作用域鏈初始化爲 sum 函數的 [[scope]] 所包含的對象。然後創建一個 與這個執行環境相關的 **變量對象( 活動對象 )** ,這個 **變量對象** 中存儲了在這個函數中定義的所有參數、變量和函數。把 **變量對象** 存儲在作用域中的頂端。  以後在查找變量的時候,總是從作用域鏈條的頂端開始查找,一直到作用域鏈條的末端。

看下面的示意圖:

說明:

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

        }

        return sum;
    }

    var sum = sum(3, 4);
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

內部函數的作用域:

函數執行後的作用域示意圖不再畫出。

四、閉包

看下面的代碼:

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

    var sumFun = createSumFunction(3, 4);
    var sum = sumFun();
    alert(sum);
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

​ 閉包是指有權訪問另一個函數作用域中的變量的函數。

五、閉包的應用

5.1 返回外部函數的局部變量

<script type="text/javascript">
    function outer () {
        var num = 5;
        //定義一個內部函數
        function inner () {
            //內部函數的返回值是外部函數的一個局部變量
            return num;
        }
        //把局部變量的值++
        num++;
        // 返回內部函數
        return inner;
    }
    var num = outer()();  // 6
    alert(num);  
</script>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

說明:

  1. 這例子中,雖然函數的聲明在num++之前,但是函數返回的時候num已經++過了,所以只是num自增之後的值。
  2. 結論:閉包中使用的局部變量的值,一定是局部變量的組後的值。

5.2 使用函數自執行和閉包封裝對象

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

<script type="text/javascript">
    var person = (function () {
        //聲明一個對象,增刪改查均是針對這個對象
        var personInfo = {
            name : "李四",
            age : 20
        };
        //返回一個對象,這個對象中封裝了一些對personInfor操作的方法
        return {
            //根據給定的屬性獲取這個屬性的值
            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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

5.3 for循環典型問題

看下面的代碼

<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> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

發現在點擊三個按鈕的時候都是彈出 我是第4個按鈕。 爲什麼呢?閉包導致的! 每循環一次都會有一個匿名函數設置點擊事件,閉包總是保持的變量的最後一個值,所以點擊的時候,總是讀的是 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的值沒有關係了。
            btns[i].index = i;
            btns[i].onclick = function () {
                alert("我是第" + (this.index + 1) + "個按鈕");
            };
        }
    </script>
</body>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

解決方案2:使用匿名函數的自執行

<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 的值傳入到num中,注意是i的值,所以num
            (function (num) {
                btns[i].onclick = function () {
                    alert("我是第" + (num + 1) + "個按鈕");
                }
            })(i);
        }
    </script>
</body>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章