作用域 作用域鏈和閉包

變量

變量分為全局變量和局部變量,全局變量就是指該變量的作用域為當(dāng)前文檔,也就是說全局變量在當(dāng)前文檔的所有JavaScript腳本都可以訪問到,都是有定義的。換言之,就是說全局變量能夠被訪問到的區(qū)域就是全局變量的作用域。

如下例子:

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

這段代碼并不會報錯,而是輸出undefined。這是為什么呢?

因為這是聲明提前引起的。所有的全局變量的聲明都會提前到JavaScript的前端聲明,而且會早于其他一切代碼。但是聲明提前了,但是對全局變量的賦值的位置不會提前,賦值的位置仍然是原位置。將上面代碼拆解如下:

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

而局部變量指的是在函數(shù)內(nèi)部聲明的變量,其中函數(shù)的形參變量也是局部變量。局部變量的作用域是該局部變量所在的整個函數(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>

如上例子,局部變量的聲明也會提前到該局部變量所在的函數(shù)的最頂端。

介紹了全局變量和局部變量,要是全局變量和局部變量重名怎么辦?如下例子:

<script type="text/javascript">
    var m = 10;
    function f(){
        var m = 20;
        alert(window.m);  //訪問同名的全局變量。其實這個時候相當(dāng)于在訪問window這個對象的屬性。
    }
    f();  
</script>

如上例子,如果局部變量和全局變量重名了,局部變量會覆蓋全局變量,而我們想要在函數(shù)中訪問同名的全局變量時,可以使用window.全局變量名。

變量的作用域

如上所述,變量的作用域就是變量能被訪問(能夠起作用)的區(qū)域。但是要注意的是JavaScript中沒有塊級作用域,這是與其他強(qiáng)類型的語言不同的,不是大括號括起來的作用域就是塊級作用域,因為根本沒有塊級作用域,所以大括號括起來的內(nèi)部的變量也是全局變量。記住一點(diǎn):函數(shù)內(nèi)部的變量才是局部變量(包括形參)。

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

如上例子,n一樣可以正常輸出10,因為沒有塊級作用域,大括號里面的變量也是全局變量,當(dāng)然就可以正常訪問了。

作用域鏈

明白了以上的作用域的話,下面介紹一下作用域鏈。

我們碼代碼要有一個寫代碼的開發(fā)環(huán)境IDE,同樣,我們的代碼在執(zhí)行過程中也需要一個執(zhí)行環(huán)境,而每一個執(zhí)行環(huán)境都有與之關(guān)聯(lián)的變量對象。而我們的執(zhí)行環(huán)境中所有的函數(shù)和變量都保存在這個變量對象中,但是我們沒法訪問這個對象。

在web瀏覽器中,全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境,該環(huán)境也被認(rèn)為是window對象,因此全局執(zhí)行環(huán)境的變量對象就是window對象。而對每個函數(shù)而言,都有自己的執(zhí)行環(huán)境,也就是該函數(shù)的內(nèi)部。每當(dāng)執(zhí)行一個函數(shù),就會進(jìn)入該函數(shù)的執(zhí)行環(huán)境。

而作用域鏈?zhǔn)桥c執(zhí)行環(huán)境相關(guān)的。在JavaScript中,“一切皆對象”,而函數(shù)的這個對象有一個內(nèi)部屬性[[scope]],該內(nèi)部屬性指向了該函數(shù)的作用域鏈,而作用域鏈中存儲了每個執(zhí)行環(huán)境相關(guān)的變量對象。

每當(dāng)創(chuàng)建(或聲明)一個函數(shù)的時候,那么會創(chuàng)建這個函數(shù)的作用域鏈,而此時這個作用域鏈中只包含了一個變量對象(window)。如下例子和圖示。

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

函數(shù)sum的作用域鏈?zhǔn)疽鈭D:
這里寫圖片描述

以上是在創(chuàng)建(或聲明)一個函數(shù)時會創(chuàng)建一個作用域鏈,而當(dāng)函數(shù)被調(diào)用時,也就是進(jìn)入到一個新的執(zhí)行環(huán)境的時候,此時這個執(zhí)行環(huán)境也就會有一個新的變量對象被創(chuàng)建,而這個對象就會被存儲在該函數(shù)的[[scope]]屬性所指向的作用域鏈中,而之前的對象就被壓在了新的變量對象的下邊,這個可以類比棧,新的變量對象就放在了棧的最頂端,給最頂端的序號為0,向下以此類推,有點(diǎn)像倒金字塔模型。如下例子:

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

函數(shù)sum在被調(diào)用時的作用域鏈?zhǔn)疽鈭D:
這里寫圖片描述

如上圖所示,當(dāng)sum函數(shù)一執(zhí)行,他的新的活動對象會被創(chuàng)建,該活動對象會處于作用域鏈的最頂端,序號為0(金字塔頂端/倒金字塔的底端)。

當(dāng)以后我們需要查找變量的時候,就總是會沿著這個作用域鏈的頂端(序號0/棧頂)開始查找,一直到作用域鏈(棧底)的末端,直到找到為止。也就是說頂端的活動對象可以訪問到其后面的參數(shù)。

<script type="text/javascript">
    var a = 8;
    function sum(num1, num2){
        var sum = num1 + num2;
        console.log(a);
        return sum;
    }
    var sum = sum(3, 4);
</script>

如上例子,當(dāng)執(zhí)行sum函數(shù)時,要打印a個變量,此時會從作用域鏈的頂端(TOP),也就是sum函數(shù)的活動對象開始查找,找不到就向作用域鏈的末端(BOTTOM)查找,直到找到為止。

閉包

閉包,一句話簡單概括就是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。話不多說,看一段例子就明白了。

<script type="text/javascript">
    function createSumFunction(num1, num2){
        return function () {
            return num1 + num2;
        };
    }
    var sumFun = createSumFunction(3, 4);
    var sum = sumFun();
    alert(sum); //輸出7
</script>

如上面的例子,createSumFunction函數(shù)返回了一個匿名函數(shù),而在匿名函數(shù)中,使用了createSumFunction函數(shù)的局部變量,但是在createSumFunction函數(shù)執(zhí)行結(jié)束之后,匿名函數(shù)仍然可以使用局部變量num1和num2,而這個匿名函數(shù)就是閉包。有人肯定也會問為什么可以該匿名函數(shù)還可以使用那些局部變量?大家別忘了上文說過的作用域鏈,由于匿名函數(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>

上面例子中,雖然函數(shù)inner聲明在num++之前,但是聲明了沒有調(diào)用,在inner函數(shù)返回的時候,num已經(jīng)是自增之后的值了。因此:閉包中使用的局部變量的值,一定是局部變量的最后的值。
閉包的一些應(yīng)用

因為閉包的存在,會給我們的應(yīng)用中帶來一些不必要的麻煩。比如下面的例子:

<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> 

如上面的例子,在實際點(diǎn)擊button的時候,彈出的都是”我是第4個按鈕”,這又是為什么呢?還是因為閉包導(dǎo)致的,給onclick賦值的函數(shù)內(nèi)部已經(jīng)訪問了另一個外部作用域的變量i,而閉包中使用的局部變量的值,一定是局部變量的最后的值。因此點(diǎn)擊的時候,總是會讀取i的最后一個值3,因此造成了每次點(diǎn)擊都是“第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>

因為閉包的存在,也可以給我們帶來一些便利,比如使用函數(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># 變量

變量分為全局變量和局部變量,全局變量就是指該變量的作用域為當(dāng)前文檔,也就是說全局變量在當(dāng)前文檔的所有JavaScript腳本都可以訪問到,都是有定義的。換言之,就是說全局變量能夠被訪問到的區(qū)域就是全局變量的作用域。

如下例子:

<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>

這段代碼并不會報錯,而是輸出undefined。這是為什么呢?

因為這是聲明提前引起的。所有的全局變量的聲明都會提前到JavaScript的前端聲明,而且會早于其他一切代碼。但是聲明提前了,但是對全局變量的賦值的位置不會提前,賦值的位置仍然是原位置。將上面代碼拆解如下:

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

而局部變量指的是在函數(shù)內(nèi)部聲明的變量,其中函數(shù)的形參變量也是局部變量。局部變量的作用域是該局部變量所在的整個函數(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>

如上例子,局部變量的聲明也會提前到該局部變量所在的函數(shù)的最頂端。

介紹了全局變量和局部變量,要是全局變量和局部變量重名怎么辦?如下例子:

<script type="text/javascript">
    var m = 10;
    function f(){
        var m = 20;
        alert(window.m);  //訪問同名的全局變量。其實這個時候相當(dāng)于在訪問window這個對象的屬性。
    }
    f();  
</script>

如上例子,如果局部變量和全局變量重名了,局部變量會覆蓋全局變量,而我們想要在函數(shù)中訪問同名的全局變量時,可以使用window.全局變量名。

變量的作用域

如上所述,變量的作用域就是變量能被訪問(能夠起作用)的區(qū)域。但是要注意的是JavaScript中沒有塊級作用域,這是與其他強(qiáng)類型的語言不同的,不是大括號括起來的作用域就是塊級作用域,因為根本沒有塊級作用域,所以大括號括起來的內(nèi)部的變量也是全局變量。記住一點(diǎn):函數(shù)內(nèi)部的變量才是局部變量(包括形參)。

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

如上例子,n一樣可以正常輸出10,因為沒有塊級作用域,大括號里面的變量也是全局變量,當(dāng)然就可以正常訪問了。

作用域鏈

明白了以上的作用域的話,下面介紹一下作用域鏈。

我們碼代碼要有一個寫代碼的開發(fā)環(huán)境IDE,同樣,我們的代碼在執(zhí)行過程中也需要一個執(zhí)行環(huán)境,而每一個執(zhí)行環(huán)境都有與之關(guān)聯(lián)的變量對象。而我們的執(zhí)行環(huán)境中所有的函數(shù)和變量都保存在這個變量對象中,但是我們沒法訪問這個對象。

在web瀏覽器中,全局執(zhí)行環(huán)境是最外圍的一個執(zhí)行環(huán)境,該環(huán)境也被認(rèn)為是window對象,因此全局執(zhí)行環(huán)境的變量對象就是window對象。而對每個函數(shù)而言,都有自己的執(zhí)行環(huán)境,也就是該函數(shù)的內(nèi)部。每當(dāng)執(zhí)行一個函數(shù),就會進(jìn)入該函數(shù)的執(zhí)行環(huán)境。

而作用域鏈?zhǔn)桥c執(zhí)行環(huán)境相關(guān)的。在JavaScript中,“一切皆對象”,而函數(shù)的這個對象有一個內(nèi)部屬性[[scope]],該內(nèi)部屬性指向了該函數(shù)的作用域鏈,而作用域鏈中存儲了每個執(zhí)行環(huán)境相關(guān)的變量對象。

每當(dāng)創(chuàng)建(或聲明)一個函數(shù)的時候,那么會創(chuàng)建這個函數(shù)的作用域鏈,而此時這個作用域鏈中只包含了一個變量對象(window)。如下例子和圖示。

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

在創(chuàng)建(或聲明)一個函數(shù)時會創(chuàng)建一個作用域鏈,而當(dāng)函數(shù)被調(diào)用時,也就是進(jìn)入到一個新的執(zhí)行環(huán)境的時候,此時這個執(zhí)行環(huán)境也就會有一個新的變量對象被創(chuàng)建,而這個對象就會被存儲在該函數(shù)的[[scope]]屬性所指向的作用域鏈中,而之前的對象就被壓在了新的變量對象的下邊,這個可以類比棧,新的變量對象就放在了棧的最頂端,給最頂端的序號為0,向下以此類推,有點(diǎn)像倒金字塔模型。如下例子:

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

當(dāng)sum函數(shù)一執(zhí)行,他的新的活動對象會被創(chuàng)建,該活動對象會處于作用域鏈的最頂端,序號為0(金字塔頂端/倒金字塔的底端)。

當(dāng)以后我們需要查找變量的時候,就總是會沿著這個作用域鏈的頂端(序號0/棧頂)開始查找,一直到作用域鏈(棧底)的末端,直到找到為止。也就是說頂端的活動對象可以訪問到其后面的參數(shù)。

<script type="text/javascript">
    var a = 8;
    function sum(num1, num2){
        var sum = num1 + num2;
        console.log(a);
        return sum;
    }
    var sum = sum(3, 4);
</script>

如上例子,當(dāng)執(zhí)行sum函數(shù)時,要打印a個變量,此時會從作用域鏈的頂端(TOP),也就是sum函數(shù)的活動對象開始查找,找不到就向作用域鏈的末端(BOTTOM)查找,直到找到為止。

閉包

閉包,一句話簡單概括就是指有權(quán)訪問另一個函數(shù)作用域中的變量的函數(shù)。話不多說,看一段例子就明白了。

<script type="text/javascript">
    function createSumFunction(num1, num2){
        return function () {
            return num1 + num2;
        };
    }
    var sumFun = createSumFunction(3, 4);
    var sum = sumFun();
    alert(sum); //輸出7
</script>

如上面的例子,createSumFunction函數(shù)返回了一個匿名函數(shù),而在匿名函數(shù)中,使用了createSumFunction函數(shù)的局部變量,但是在createSumFunction函數(shù)執(zhí)行結(jié)束之后,匿名函數(shù)仍然可以使用局部變量num1和num2,而這個匿名函數(shù)就是閉包。有人肯定也會問為什么可以該匿名函數(shù)還可以使用那些局部變量?大家別忘了上文說過的作用域鏈,由于匿名函數(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>

上面例子中,雖然函數(shù)inner聲明在num++之前,但是聲明了沒有調(diào)用,在inner函數(shù)返回的時候,num已經(jīng)是自增之后的值了。因此:閉包中使用的局部變量的值,一定是局部變量的最后的值。

閉包的一些應(yīng)用
因為閉包的存在,會給我們的應(yīng)用中帶來一些不必要的麻煩。比如下面的例子:

<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> 

如上面的例子,在實際點(diǎn)擊button的時候,彈出的都是”我是第4個按鈕”,這又是為什么呢?還是因為閉包導(dǎo)致的,給onclick賦值的函數(shù)內(nèi)部已經(jīng)訪問了另一個外部作用域的變量i,而閉包中使用的局部變量的值,一定是局部變量的最后的值。因此點(diǎn)擊的時候,總是會讀取i的最后一個值3,因此造成了每次點(diǎn)擊都是“第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>

因為閉包的存在,也可以給我們帶來一些便利,比如使用函數(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>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容