範圍和閉包的通俗理解

來源:互聯網
上載者:User

範圍和閉包的通俗理解
前言

面試問題:

  • 說一下對變數提升的理解
  • 說明this的幾種不同的使用情境
  • 建立10個<a>標籤,點擊的時候彈出來對應的序號
  • 如何理解範圍
  • 實際開發中閉包的應用

涉及到的知識點:

  • 執行內容
  • this
  • 範圍
  • 範圍鏈
  • 閉包
執行內容

執行內容主要有兩種情況:

  • 全域代碼: 一段<script>標籤裡,有一個全域的執行內容。所做的事情是:變數定義、函式宣告
  • 函數代碼:每個函數裡有一個上下文。所做的事情是:變數定義、函式宣告、this、arguments

PS:注意“函式宣告”和“函數運算式”的區別。

全域執行內容

在執行全域代碼前將window確定為全域執行內容。

(1)對全域資料進行預先處理:(並沒有賦值)

  • var定義的全域變數==>undefined, 添加為window的屬性
  • function聲明的全域函數==>賦值(fun), 添加為window的方法
  • this==>賦值(window)

(2)開始執行全域代碼

函數執行內容

在調用函數, 準備執行函數體之前, 建立對應的函數執行內容對象(虛擬, 存在於棧中)。

(1)對局部資料進行預先處理:

  • 形參變數==>賦值(實參)==>添加為執行內容的屬性
  • arguments==>賦值(實參列表), 添加為執行內容的屬性
  • var定義的局部變數==>undefined, 添加為執行內容的屬性
  • function聲明的函數 ==>賦值(fun), 添加為執行內容的方法
  • this==>賦值(調用函數的對象)

(2)開始執行函數體代碼

執行內容棧
  • 1.在全域代碼執行前, JS引擎就會建立一個棧來儲存管理所有的執行內容對象
  • 2.在全域執行內容(window)確定後, 將其添加到棧中(壓棧)
  • 3.在函數執行內容建立後, 將其添加到棧中(壓棧)
  • 4.在當前函數執行完後,將棧頂的對象移除(出棧)
  • 5.當所有的代碼執行完後, 棧中只剩下window
this

this指的是,調用函數的那個對象。

需要特別提醒的是:this的指向在函數定義時無法確認,只有函數執行時才能確定。

 

this的幾種情境:

  • 1、作為建構函式執行

例如:

    functionFoo(name) {
        //this = {};
        this.name = name;
        //return this;
    }

    var foo = new Foo();

  • 2、作為對象的屬性執行

    var obj = {
        name: 'A',
        printName: function () {
            console.log(this.name);
        }
    }

    obj.printName();

  • 3、作為普通函數執行

    functionfn() {
        console.log(this); //this === window
    }

    fn();

  • 4、call apply bind
範圍

範圍指一個變數的作用範圍。它是靜態(相對於內容物件), 在編寫代碼時就確定了。

作用:隔離變數,不同範圍下同名變數不會有衝突。

範圍的分類:

  • 全域範圍
  • 函數範圍
  • 沒有塊範圍(ES6有了)

if (true) {
    var name = 'smyhvae';
}
console.log(name);

上方代碼中,並不會報錯,因為:雖然 name 是在塊裡面定義的,但是 name 是全域變數。

全域範圍

直接編寫在script標籤中的JS代碼,都在全域範圍。

在全域範圍中:

  • 在全域範圍中有一個全域對象window,它代表的是一個瀏覽器的視窗,它由瀏覽器建立我們可以直接使用。
  • 建立的變數都會作為window對象的屬性儲存。
  • 建立的函數都會作為window對象的方法儲存。

全域範圍中的變數都是全域變數,在頁面的任意的部分都可以訪問到。

變數的聲明提前:

使用var關鍵字聲明的變數( 比如 var a = 1),會在所有的代碼執行之前被聲明(但是不會賦值),但是如果聲明變數時不是用var關鍵字(比如直接寫a = 1),則變數不會被聲明提前。

舉例1:

    console.log(a);
    var a = 123;

列印結果:undefined

舉例2:

    console.log(a);
    a = 123;  //此時a相當於window.a

程式會報錯:

函數的聲明提前:

  • 使用函式宣告的形式建立的函數function foo(){},會被聲明提前。

也就是說,它會在所有的代碼執行之前就被建立,所以我們可以在函式宣告之前,調用函數。

  • 使用函數運算式建立的函數var foo = function(){},不會被聲明提前,所以不能在聲明前調用。

很好理解,因為此時foo被聲明了,且為undefined,並沒有給其賦值function(){}

所以說,下面的例子,會報錯:

函數範圍

調用函數時建立函數範圍,函數執行完畢以後,函數範圍銷毀。

每調用一次函數就會建立一個新的函數範圍,他們之間是互相獨立的。

在函數範圍中可以訪問到全域範圍的變數,在全域範圍中無法訪問到函數範圍的變數。

在函數中要訪問全域變數可以使用window對象。(比如說,全域範圍和函數範圍都定義了變數a,如果想訪問全域變數,可以使用window.a

提醒1:

在函數範圍也有聲明提前的特性:

  • 使用var關鍵字聲明的變數,會在函數中所有的代碼執行之前被聲明
  • 函式宣告也會在函數中所有的代碼執行之前執行

因此,在函數中,沒有var聲明的變數都會成為全域變數,而且並不會提前聲明。

舉例1:

        var a = 1;

        functionfoo() {
            console.log(a);
            a = 2;    // 此處的a相當於window.a
        }

        foo();
        console.log(a);  //列印結果是2上方代碼中,foo()的列印結果是1。如果去掉第一行代碼,列印結果是Uncaught ReferenceError: a is not defined

提醒2:定義形參就相當於在函數範圍中聲明了變數。

        function fun6(e) {
            console.log(e);
        }

        fun6();  //列印結果為 undefined
        fun6(123);//列印結果為123

範圍與執行內容的區別

區別1:

  • 全域範圍之外,每個函數都會建立自己的範圍,範圍在函數定義時就已經確定了。而不是在函數調用時
  • 全域執行內容環境是在全域範圍確定之後, js代碼馬上執行之前建立
  • 函數執行內容是在調用函數時, 函數體代碼執行之前建立

區別2:

  • 範圍是靜態, 只要函數定義好了就一直存在, 且不會再變化
  • 執行內容是動態, 調用函數時建立, 函數調用結束時就會自動釋放

聯絡:

  • 執行內容(對象)是從屬於所在的範圍
  • 全域上下文環境==>全域範圍
  • 函數上下文環境==>對應的函數使用域
範圍鏈

當在函數範圍操作一個變數時,它會先在自身範圍中尋找,如果有就直接使用(就近原則)。如果沒有則向上一級範圍中尋找,直到找到全域範圍;如果全域範圍中依然沒有找到,則會報錯ReferenceError。

外部函數定義的變數可以被內建函式所使用,反之則不行。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script>//只要是函數就可以創造範圍//函數中又可以再建立函數//函數內部的範圍可以訪問函數外部的範圍//如果有多個函數嵌套,那麼就會構成一個鏈式訪問結構,這就是範圍鏈//f1--->全域functionf1(){//f2--->f1--->全域functionf2(){//f3---->f2--->f1--->全域functionf3(){}//f4--->f2--->f1---->全域functionf4(){}}//f5--->f1---->全域functionf5(){}}</script>
</head>
<body>

</body>
</html>

理解:

  • 多個上下級關係的範圍形成的鏈, 它的方向是從下向上的(從內到外)
  • 尋找變數時就是沿著範圍鏈來尋找的

尋找一個變數的尋找規則:

    var a = 1

    functionfn1() {
      var b = 2

      functionfn2() {
        var c = 3
        console.log(c)
        console.log(b)
        console.log(a)
        console.log(d)
      }
      fn2()
    }
    fn1()

  • 在當前範圍下的執行內容中尋找對應的屬性, 如果有直接返回, 否則進入2
  • 在上一級範圍的執行內容中尋找對應的屬性, 如果有直接返回, 否則進入3
  • 再次執行2的相同操作, 直到全域範圍, 如果還找不到就拋出找不到的異常
閉包

閉包就是能夠讀取其他函數內部資料(變數/函數)的函數。

只有函數內部的子函數才能讀取局部變數,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。

上面這兩句話,是阮一峰的文章裡的,你不一定能理解,來看下面的講解和舉例。

如何產生閉包

當一個嵌套的內部(子)函數引用了嵌套的外部(父)函數的變數或函數時, 就產生了閉包。

閉包到底是什麼?

使用chrome調試查看

  • 理解一: 閉包是嵌套的內建函式(絕大部分人)
  • 理解二: 包含被引用變數 or 函數的對象(極少數人)

注意: 閉包存在於嵌套的內建函式中。

產生閉包的條件
  • 1.函數嵌套
  • 2.內建函式引用了外部函數的資料(變數/函數)。

來看看條件2:

    functionfn1() {
        functionfn2() {

        }

        return fn2;
    }

    fn1();

上面的代碼不會產生閉包,因為內建函式fn2並沒有引用外部函數fn1的變數。

PS:還有一個條件是外部函數被調用,內建函式被聲明。比如:

    functionfn1() {
        var a = 2
        var b = 'abc'

        functionfn2() { //fn2內建函式被提前聲明,就會產生閉包(不用調用內建函式)
            console.log(a)
        }

    }

    fn1();

    functionfn3() {
        var a = 3
        var fun4 = function () {  //fun4採用的是“函數運算式”建立的函數,此時內建函式的聲明並沒有提前
            console.log(a)
        }
    }

    fn3();

常見的閉包
    1. 將一個函數作為另一個函數的傳回值
    1. 將函數作為實參傳遞給另一個函數調用。
閉包1:將一個函數作為另一個函數的傳回值

    functionfn1() {
      var a = 2

      functionfn2() {
        a++
        console.log(a)
      }
      return fn2
    }

    var f = fn1();  //執行外部函數fn1,返回的是內建函式fn2
    f() // 3      //執行fn2
    f() // 4      //再次執行fn2

當f()第二次執行的時候,a加1了,也就說明了:閉包裡的資料沒有消失,而是儲存在了記憶體中。如果沒有閉包,代碼執行完倒數第三行後,變數a就消失了。

上面的代碼中,雖然調用了內建函式兩次,但是,閉包對象只建立了一個。

也就是說,要看閉包對象建立了一個,就看:外部函數執行了幾次(與內建函式執行幾次無關)。

閉包2. 將函數作為實參傳遞給另一個函數調用

    functionshowDelay(msg, time) {
      setTimeout(function() {  //這個function是閉包,因為是嵌套的子函數,而且引用了外部函數的變數msg
        alert(msg)
      }, time)
    }
    showDelay('atguigu', 2000)

上面的代碼中,閉包是裡面的funciton,因為它是嵌套的子函數,而且引用了外部函數的變數msg。

閉包的作用
  • 作用1. 使用函數內部的變數在函數執行完後, 仍然存活在記憶體中(延長了局部變數的生命週期)
  • 作用2. 讓函數外部可以操作(讀寫)到函數內部的資料(變數/函數)

我們讓然拿這段代碼來分析:

    functionfn1() {
      var a = 2

      functionfn2() {
        a++
        console.log(a)
      }
      return fn2
    }

    var f = fn1();  //執行外部函數fn1,返回的是內建函式fn2
    f() // 3      //執行fn2
    f() // 4      //再次執行fn2

作用1分析:

上方代碼中,外部函數fn1執行完畢後,變數a並沒有立即消失,而是儲存在記憶體當中。

作用2分析:

函數fn1中的變數a,是在fn1這個函數範圍內,因此外部無法訪問。但是通過閉包,外部就可以操作到變數a。

達到的效果是:外界看不到變數a,但可以操作a。

比如上面達到的效果是:我看不到變數a,但是每次執行函數後,讓a加1。當然,如果我真想看到a,我可以在fn2中將a返回即可。

回答幾個問題:

  • 問題1. 函數執行完後, 函數內部聲明的局部變數是否還存在?

答案:一般是不存在, 存在於閉中的變數才可能存在。

閉包能夠一直存在的根本原因是f,因為f接收了fn1(),這個是閉包,閉包裡有a。注意,此時,fn2並不存在了,但是裡面的對象(即閉包)依然存在,因為用f接收了。

  • 問題2. 在函數外部能直接存取函數內部的局部變數嗎?

不能,但我們可以通過閉包讓外部操作它。

閉包的生命週期
  • 產生: 嵌套內建函式fn2被聲明時就產生了(不是在調用)
  • 死亡: 嵌套的內建函式成為垃圾對象時。(比如f = null,就可以讓f成為垃圾對象。意思是,此時f不再引用閉包這個對象了)
閉包的應用:定義具有特定功能的js模組
  • 將所有的資料和功能都封裝在一個函數內部(私人的),只向外暴露一個包含n個方法的對象或函數。
  • 模組的使用者, 只需要通過模組暴露的對象調用方法來實現對應的功能。
方式一

(1)myModule.js:(定義一個模組,向外暴露多個函數,供外界調用)

functionmyModule() {
    //私人資料
    var msg = 'Smyhvae Haha'

    //操作私人資料的函數
    functiondoSomething() {
        console.log('doSomething() ' + msg.toUpperCase()); //字串大寫
    }

    functiondoOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase()) //字串小寫
    }

    //通過【對象字面量】的形式進行包裹,向外暴露多個函數
    return {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
}

上方代碼中,外界可以通過doSomething1和doOtherthing2來操作裡面的資料,但不讓外界看到。

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_閉包的應用_自訂JS模組</title>
</head>
<body>
<!--閉包的應用 : 定義JS模組  * 具有特定功能的js檔案  * 將所有的資料和功能都封裝在一個函數內部(私人的)  * 【重要】只向外暴露一個包含n個方法的對象或函數  * 模組的使用者, 只需要通過模組暴露的對象調用方法來實現對應的功能-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">var module =myModule();module.doSomething1();module.doOtherthing2();</script>
</body>
</html>

方式二

同樣是實現方式一種的功能,這裡我們採取另外一種方式。

(1)myModule2.js:(是一個立即執行的匿名函數)

(function () {
    //私人資料
    var msg = 'Smyhvae Haha'

    //操作私人資料的函數
    functiondoSomething() {
        console.log('doSomething() ' + msg.toUpperCase())
    }

    functiondoOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase())
    }

    //外部函數是即使啟動並執行匿名函數,我們可以把兩個方法直接傳給window對象
    window.myModule = {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
})()

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>05_閉包的應用_自訂JS模組2</title>
</head>
<body>
<!--閉包的應用2 : 定義JS模組  * 具有特定功能的js檔案  * 將所有的資料和功能都封裝在一個函數內部(私人的)  * 只向外暴露一個包信n個方法的對象或函數  * 模組的使用者, 只需要通過模組暴露的對象調用方法來實現對應的功能-->

<!--引入myModule檔案-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">myModule.doSomething1()
    myModule.doOtherthing2()
</script>
</body>
</html>

上方兩個檔案中,我們在myModule2.js裡直接把兩個方法直接傳遞給window對象了。於是,在index.html中引入這個js檔案後,會立即執行裡面的匿名函數。在index.html中把myModule直接拿來用即可。

總結:

當然,方式一和方式二對比後,我們更建議採用方式二,因為很方便。

但無論如何,兩種方式都採用了閉包。

閉包的缺點及解決

缺點:函數執行完後, 函數內的局部變數沒有釋放,佔用記憶體時間會變長,容易造成記憶體泄露。

解決:能不用閉包就不用,及時釋放。比如:

    f = null;  // 讓內建函式成為垃圾對象 -->回收閉包

總而言之,你需要它,就是優點;你不需要它,就成了缺點。

記憶體流失記憶體溢出記憶體流失

記憶體流失:佔用的記憶體沒有及時釋放。記憶體泄露積累多了就容易導致記憶體溢出。

常見的記憶體泄露:

  • 1.意外的全域變數
  • 2.沒有及時清理的計時器或回呼函數
  • 3.閉包

情況1舉例:

    // 意外的全域變數
    functionfn() {
        a = new Array(10000000);
        console.log(a);
    }

    fn();

情況2舉例:

    // 沒有及時清理的計時器或回呼函數
    var intervalId = setInterval(function () { //啟動迴圈定時器後不清理
        console.log('----')
    }, 1000)

    // clearInterval(intervalId);  //清理定時器

情況3舉例:

<script type="text/javascript">functionfn1() {var arr =new Array[100000];//這個數組佔用了很大的記憶體空間functionfn2() {console.log(arr.length)
    }return fn2
  }var f =fn1()
  f()

  f =null//讓內建函式成為垃圾對象-->回收閉包</script>

記憶體溢出(一種程式運行出現的錯誤)

記憶體溢出:當程式運行需要的記憶體超過了剩餘的記憶體時,就出拋出記憶體溢出的錯誤。

  //記憶體溢出
  var obj = {}
  for (var i = 0; i < 10000; i++) {
    obj[i] = new Array(10000000);  //把所有的數組內容都放到obj裡儲存,導致obj佔用了很大的記憶體空間
    console.log('-----')
  }

本文永久更新連結地址:https://www.bkjia.com/Linux/2018-03/151400.htm

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.