5-閉包和作用域

作用域概念

1. 作用域:函數(shù)內(nèi)的可用變量

- 函數(shù)的傳參
- 函數(shù)內(nèi)的局部變量
- 父函數(shù)作用域的變量
- 全局變量

2. 作用域鏈:

- 解析變量時,js引擎先查看嵌套子函數(shù)內(nèi)的局部變量和參數(shù),如果能找到就檢索該值。
- 否則沿著作用域鏈向上查找,父函數(shù)作用域,一直到全局變量,直到變量被解析。
- 如果js引擎以達(dá)到全局作用域,仍無法解析,則該變量為未定義,undefined。
- 順序:局部變量 => 父函數(shù)中變量 => 父函數(shù)的父函數(shù)的變量 => ··· => 全局變量

3. 變量陰影:

- 在函數(shù)內(nèi)的變量與函數(shù)外的變量重復(fù)時,函數(shù)內(nèi)的變量會暫時遮蔽外部作用域中的變量,稱為變量陰影。
- 局部變量優(yōu)先于更寬作用域內(nèi)與其同名的變量

4. 自由變量:

嵌套函數(shù)可以捕獲并未作為參數(shù)傳入,也不在局部函數(shù)定義的變量,成為自由變量。

閉包

1. 簡單理解閉包

簡單理解閉包的概念,函數(shù)保留對其作用域(鏈)訪問的過程被稱為閉包。
或者可以這樣理解:函數(shù)本身,及函數(shù)聲明位置的代碼(其上一級的作用域)。
閉包特性:函數(shù)至少會在作用域鏈上遮蔽另一個上下文:全局作用域。這也是為什么全局變量可以被任何函數(shù)訪問到的原因。
但只有在嵌套函數(shù)(在一個函數(shù)中定義另一個函數(shù))中,閉包的功能才會發(fā)揮出來。

需要特別指出的是:
函數(shù)會保持對其父作用域的引用。如果能可以訪問該函數(shù)的引用,作用域就會保持不變
// 兩個例子
// 例1
var outValue = "ninja";
function outFunction(){
    console.log(outValue); //ninja
}
// 函數(shù)可以訪問到全局作用域的變量,是我們一直知道的。
// 但是原因是因為閉包,閉包允許函數(shù)訪問并操作函數(shù)外部的變量,只要變量或函數(shù)存在于聲明函數(shù)時的作用域內(nèi)。
// 也可以理解為變量outValue存在于函數(shù)父作用域內(nèi)。

// 例2
var outValue = "sam";
var later;
function outer(){
    var innerValue = 'ninja';
    function inner(){
        console.log(outValue);   // sam 全局作用域
        console.log(innerValue); // ninja 父作用域
    } 
    later = inner;
}
outer(); // inner函數(shù)賦給了later
later(); // 因為inner與innerValue在同一作用域下|
//原理流程是:
//當(dāng)外部函數(shù)中聲明內(nèi)部函數(shù)式,不僅聲明了內(nèi)部函數(shù),還創(chuàng)建了一個閉包:
//該閉包不僅包含了函數(shù)的聲明,還包含了在函數(shù)聲明時該外部函數(shù)的作用域(所有變量)。
//當(dāng)最終執(zhí)行內(nèi)部函數(shù)時,盡管聲明時的作用域已經(jīng)消失了,但是通過閉包,仍然能夠訪問原始作用域。
//每個通過閉包訪問變量的函數(shù)都有一個作用域鏈,作用域鏈包含閉包的全部信息。

2. 使用閉包

  • 封裝私有變量
    function Ninja(){
        var count = 0; //私有變量,注意不是this
        this.getCount = function () {
             return count; //通過this將方法暴露出來
        }
        this.counts = function () {
            return  count++; //先運(yùn)算再++ 因為在函數(shù)內(nèi)部聲明函數(shù),形成閉包。可以訪問同作用域下的變量/父級作用域變量
        }
    }

    var ninjia = new Ninja();
    console.log(ninjia)
  • 回調(diào)函數(shù)中使用閉包
    function animateIt(elementId){
        var elem = document.getElementById(elementId);
        var tick = 0;
        var timer = setInterval(function(){  //閉包
    
            if(tick<100){
                elem.style.marginLeft = elem.style.marginTop = tick + 'px';
                tick++;
            }else{
                clearInterval(timer);
                console.log(tick===100) //true
                console.log(elem) // <div id="box">div>
                console.log(timer) //1
            }
        },10)
    }
    animateIt('box')

3. 通過執(zhí)行上下文來跟蹤代碼

js引擎時如何跟蹤函數(shù)的執(zhí)行并回到函數(shù)的位置呢?

js代碼有兩種類型:全局代碼和函數(shù)代碼。
js引擎執(zhí)行代碼時,每一條語句都處于特定的執(zhí)行上下文中。
之前說過,js執(zhí)行代碼,從上往下執(zhí)行全局代碼,執(zhí)行到函數(shù)調(diào)用時再返回到函數(shù)定義中,執(zhí)行完成再回到函數(shù)中。

既然有兩種類型的代碼,就有兩種執(zhí)行上下文。全局執(zhí)行上下文和函數(shù)執(zhí)行上下文。
二者最主要的區(qū)別時:全局執(zhí)行上下文只有一個,函數(shù)執(zhí)行上下文是在每次調(diào)用時,都會創(chuàng)建一個新的函數(shù)執(zhí)行上下文。js引擎使用執(zhí)行上下文跟蹤函數(shù)的執(zhí)行。

注意:函數(shù)上下文是this。函數(shù)執(zhí)行上下文是內(nèi)部的js概念,js引擎使用執(zhí)行上下文來跟蹤函數(shù)的執(zhí)行。
js單線程的執(zhí)行模式:在某個特定時刻只能執(zhí)行特定的代碼。一旦發(fā)生調(diào)用,當(dāng)前執(zhí)行上下文必須停止,
并創(chuàng)建新的函數(shù)執(zhí)行上下文來執(zhí)行函數(shù)。當(dāng)函數(shù)執(zhí)行完成后,將函數(shù)執(zhí)行上下文銷毀,并重新回到生調(diào)用的執(zhí)行上下文中。
所以需要跟蹤執(zhí)行上下文-正在執(zhí)行的上下文以及正在等待的上下文。最簡單的跟蹤方法是執(zhí)行上下文棧(或稱為調(diào)用棧)-后進(jìn)先出
比如
先執(zhí)行全局代碼,調(diào)用函數(shù)時進(jìn)入函數(shù)執(zhí)行上下文,嵌套函數(shù)上下文,退出嵌套函數(shù)上下文,回到函數(shù)上下文然后退出,最后回到全局上下文。

4. 使用此法環(huán)境跟蹤變量的作用域

詞法環(huán)境:js引擎內(nèi)部用來跟蹤標(biāo)識符和特定變量之間的映射關(guān)系。
也是js作用域的內(nèi)部實現(xiàn)機(jī)制,人們常稱為作用域。
詞法環(huán)境主要基于代碼嵌套,內(nèi)部代碼結(jié)構(gòu)可以訪問外部代碼結(jié)構(gòu)中定義的變量。

詞法環(huán)境如何跟蹤這些變量的呢?
在特定的執(zhí)行上下文中,我們的程序不僅直接訪問詞法環(huán)境中定義的局部變量,而且還會訪問外部環(huán)境定義的變量。是因為-->
無論何時創(chuàng)建函數(shù),都會創(chuàng)建詞法環(huán)境,包含外部環(huán)境的引用(當(dāng)前函數(shù)所在的環(huán)境引用)。存在[[Environment]]的內(nèi)部屬性上,這是函數(shù)被創(chuàng)建時的所在環(huán)境。
調(diào)用函數(shù)時,會創(chuàng)建新的執(zhí)行上下文,也會創(chuàng)建新的詞法環(huán)境,這個詞法環(huán)境會關(guān)聯(lián)創(chuàng)建時的外部環(huán)境。
var a = 1;
function bar(){  //創(chuàng)建時bar有全局詞法環(huán)境的引用
   var a = 2;
   function foo() { //創(chuàng)建時foo有bar詞法環(huán)境的引用
    console.log(a)
   };
   foo()  //foo調(diào)用時,會關(guān)聯(lián)到foo創(chuàng)建時的詞法環(huán)境,所以2
};
bar(); //2

var a = 1;
function foo() { //創(chuàng)建時 foo有全局引用
    console.log(a)
};
function bar(){   // bar有全局引用
    var a = 2;
    foo(); // foo調(diào)用時,關(guān)聯(lián)到foo創(chuàng)建時的環(huán)境,即全局環(huán)境,所以1
};
bar(); //1

5. 理解js的變量類型

  • 變量可變性

    • const 不需要重新賦值的特殊變量/指向一個固定值
    • 聲明const變量并初始化,以后不允許對const變量重新賦值const a=[]
    • 當(dāng)const變量值為對象/數(shù)組時,可以為其添加修改屬性值,但不能重寫const變量。
    • 使用const定義全局變量,全局靜態(tài)變量通常用大寫。const GLOGAL_NINJA='yoshi'
  • 定義變量的關(guān)鍵字和詞法環(huán)境

    • 使用關(guān)鍵字var
      • 該變量是在距離最近的函數(shù)內(nèi)部或全局詞法環(huán)境中定義的。var沒有塊級作用域的概念。也就是說,for循環(huán)中的var變量在for循環(huán)外,函數(shù)內(nèi),都能訪問。
    • 使用let與const定義具有塊級作用域的變量
      • let和const是最近的詞法環(huán)境中定義變量。
      • 【可以在塊級】、【循環(huán)內(nèi)】、【函數(shù)或者全局環(huán)境】。
  • 在詞法環(huán)境中注冊標(biāo)識符

    • 注冊標(biāo)識符的過程
    js引擎在執(zhí)行代碼時分了兩個階段,
    第一個階段訪問注冊詞法環(huán)境聲明的變量和函數(shù);
    第二個階段,執(zhí)行代碼,具體執(zhí)行略;
    
    第一階段過程:
    - 判斷是否是函數(shù),創(chuàng)建形參和默認(rèn)參數(shù)。否,略過;
    - 判斷全局或函數(shù),掃描當(dāng)前代碼(非塊級作用域)進(jìn)行函數(shù)聲明,不掃描函數(shù)表達(dá)式和箭頭函數(shù)和構(gòu)造函數(shù)。
    找到函數(shù)聲明后創(chuàng)建函數(shù),并在當(dāng)前環(huán)境內(nèi)綁定標(biāo)識符,若標(biāo)識符已存在則重寫;
    - 掃描當(dāng)前代碼的變量聲明。找到函數(shù)外、塊級外var let const聲明的變量,注冊標(biāo)識符并初始化為undefined。若標(biāo)識符已存在,將保留其值。
    
  • 變量提升和函數(shù)聲明提升

    • 即在代碼執(zhí)行前現(xiàn)在詞法環(huán)境里注冊,
      但是變量不會賦值。函數(shù)也只對函數(shù)聲明有效。
  • 似有變量的使用及警告

        function Nin(){
            var num=0;
            this.getNum=function(){
                console.log('getNum',num)
                return num;
            }
            this.setNum=function(){
                 num++;
                 console.log('setNum',num)
            }
        }
        var num1=new Nin()
        console.log(num1.getNum())
        console.log(num1.setNum())
        //通過閉包的方式實現(xiàn)了私有化,但是
        // const bb=num1.getNum()
        // bb能拿到似有變量的值,所以私有化只是模擬私有化
    

6. 研究閉包原理

    function Ninja(){
        var count = 0; //私有變量,注意不是this
        this.getCount = function () {
             return count; //通過this將方法暴露出來
        }
        this.counts = function () {
            return  count++; //先運(yùn)算再++ 因為在函數(shù)內(nèi)部聲明函數(shù),形成閉包。
            //可以訪問同作用域下的變量/父級作用域變量
        }
    }

    var ninjia = new Ninja();
    console.log(ninjia)
    /*無論何時創(chuàng)建函數(shù),都會保持詞法環(huán)境的引用(通過內(nèi)置[[Environment]]),
    在本例中,Ninja構(gòu)造函數(shù)內(nèi)部,創(chuàng)建的兩個函數(shù)getFeints于feint均有Ninja環(huán)境的引用,
    因為Ninja環(huán)境是這兩個函數(shù)創(chuàng)建時所處的環(huán)境。
    getFeints與feint函數(shù)是新創(chuàng)建的ninja的對象方法,
    因此可以在Ninjs構(gòu)造函數(shù)外部訪問這兩個函數(shù)。
    這樣實際上就創(chuàng)建了包含feints變量的閉包。「閉包的原理」
    */

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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