JavaScript入門③-函數(2)原理{深入}執行上下文

image.png

00、頭痛的JS閉包、詞法作用域?

被JavaScript的閉包、上下文、嵌套函數、this搞得很頭痛,這語言設計的,感覺比較混亂,先勉強理解總結一下😂😂😂。

  • 爲什麼有閉包這麼個東西?閉包包的是什麼?
  • 什麼是詞法作用域?
  • 函數是如執行的呢?

image


01、執行上下文 (execution context)

名稱 描述
是什麼? 執行上下文 (execution context) 是JavaScript代碼被解析和執行時所在環境的抽象概念。每一個函數執行的時候都會創建自己的執行上下文,保存了函數運行時需要的信息,並通過自己的上下文來運行函數。
幹什麼用的? 當然就是運行函數自身的,實現自我價值。
有那些種類? ① 全局上下文:全局環境最基礎的執行上下文,所有不在任何函數內的代碼都在這裏面。
🔸 瀏覽器中的全局對象就是window,全局作用域下var申明、隱式申明的變量都會成爲全局屬性變量,全局的this指向window
🔸 其中會初始化一些全局對象或全局函數,如代碼中的consoleundefinedisNaN
② 函數上下文:每個函數都有自己的上下文,調用函數的時候創建。可以把全局上下文看成是一個頂級根函數上下文。
eval() 調用內部上下文eval的代碼會被編譯創建自己的執行上下文,不常用也不建議使用。基於這個特點,有些框架會用eval()來實現沙箱Sandbox。
保存了什麼信息? 初始化上下文的變量、函數等信息
🔸 thisValuethis環境對象引用。
🔸 內部(Local)環境:函數本地的所有變量、函數、參數(arguments)。
🔸 作用域鏈:具有訪問作用域的其他上下文信息。
誰來用? 執行上下文函數調用棧來統一保存和調度管理。
生命週期 創建(入棧)=> 執行=> 銷燬(出棧),函數調用的時候創建,執行完成後銷燬。

image


02、函數調用棧是幹啥的?

函數調用棧(Function Call Stack),管理函數調用的一種棧式結構(後進先出 )隊列,或稱執行棧,存儲了當前程序所有執行上下文(正在執行的函數)。最早入棧的理所當然就是程序初始化時創建的全局上下文了,他是VIP會員,會一直在棧底,直到程序退出。

2.1、函數執行流程

🟢函數執行上下文調用流程(也是函數的生命週期):

  • 創建-入棧:創建執行上下文,並壓入棧,獲得控制權。
  • 執行-幹活:執行函數的代碼,給變量賦值、查找變量。如有內部函數調用,遞歸重複函數流程。
  • 出棧-銷燬:函數執行完成出棧,釋放該執行上下文,其變量也就釋放了,全都銷燬,控制權回到上一層執行上下文。

image

function first() {
    second();	//調用second()
}
function second() {
}
first();

上面的代碼執行過程如下圖所示

  1. 程序初始化運行時,首先創建的是全局上下文Global,進入執行棧。
  2. 調用first()函數,創建其下文併入棧。
  3. first()函數內部調用了second()函數,創建second()下文入棧並執行。
  4. second()函數執行完成並出棧,控制權回到first()函數上下文。
  5. first()函數執行完成並出棧,控制權回到全局上下文。

c71b3089775edcdcd2043eba90a70572_u=544850019,275206126&fm=253&app=138&f=PNG&fmt=auto&q=75_w=1280&h=228.webp

🌰再來一個函數調用棧的示例:

var a = 1;
let b = 1;
function FA(x) {
    function FB(y) {
        function FC(z) {
            console.log(a + b + x + y + z);
        }
        FC(3);
    }
    FB(2);
}
FA(1); //8

上面函數在執行FC()時的函數調用堆棧如下圖(Edge瀏覽器斷點調試):

image.png

✅ 執行FC函數代碼時,其作用域保留了所有要用到的作用域變量,從自己往上,直到全局對象,閉包就是這麼來的!

  • var a = 1;:var申明的變量會作爲全局對象window的變量。
  • let b = 1;:全局環境申明的變量,任何函數都可以訪問,放在全局腳本環境中,可以看做全局的一部分。

✅ 調用堆棧中有FC、FB、FA,因爲是嵌套函數,FB、FA並未結束,所以還在堆棧中,函數執行完畢就會被立即釋放拋棄。

image.png

2.2、堆棧溢出

📢 函數調用棧容量是有限的!—— 遞歸函數

遞歸函數就是一個多層+自我嵌套調用的過程,所以執行遞歸函數時,會不停的入棧,而沒有出棧,循環次數太多會超出堆棧容量限制,從而引發報錯。比如下面示例中一個簡單的加法遞歸,在Firefox瀏覽器中遞歸1500次,就報錯了(InternalError: too much recursion),Edge瀏覽器是11000次超出調用棧容量(Maximum call stack size exceeded)。

❓怎麼解決呢?

  • 避免遞歸:封裝處理邏輯,轉換成循環的方式來處理。或用setTimeout(func,0)發送到任務隊列單獨執行。
  • 拆分執行:合理拆分代碼爲多個遞歸函數。
function add(x) {
    if (x <= 0)
        return 0;
    return x + add(x - 1);  //遞歸求和
}
add(1000); //Firefox:1000可以,1500就報錯 InternalError: too much recursion
add(10000);//Edge:10000可以執行,11000就報錯 Maximum call stack size exceeded

» Firefox 的調用堆棧:

image.png


03、什麼是詞法作用域?

作用域(scope)就是一套規定變量作用範圍(權限),並按此去查找變量的規則。包括靜態作用域動態作用域,JavaScript中主要是靜態作用域(詞法作用域)

  • 🔴 靜態作用域(就是詞法作用域):JavaScript是基於詞法作用域來創建作用域的,基於代碼的詞法分析確定變量的作用域、作用域關係(作用域鏈)。詞法環境就是我們寫代碼的順序,所以是靜態的,就是說函數、變量的作用域是在其申明的時候就已經確定了,在運行階段不再改變。
  • 🟡 動態作用域:基於動態調用的關係確定的,其作用域鏈是基於運行時的調用棧的。比如this,一般就是基於調用來確定上下文環境的。因此this值可以在調用棧上來找,注意的是this指向一個引用對象,不是函數本身,也不是其詞法作用域。

image

因此,詞法作用域主要作用是規定了變量的訪問權限,確定瞭如何去查找變量,基本規則

  • 代碼位置決定:變量(包括函數)申明的的地方決定了作用域,跟在哪調用無關。
  • 擁有父級權限:函數(或塊)可以訪問其外部的數據,如果嵌套多層,則遞歸擁有父級的作用域權限,直到全局環境。
  • 函數作用域:只有函數可以限定作用域,不能被上級、外部其他函數訪問。
  • 同名就近使用:如果有和上級同名的變量,則就近使用,先找到誰就用誰。
  • 逐層向上查找:變量的查找規則就是先內部,然逐級往上,直到全局環境,如果都沒找到,則變量undefined

這裏的詞法作用域,就是前文所說JS變量作用域。而閉包保留了上下文作用域的變量,就是爲了實現詞法作用域。

❓那詞法作用域是怎麼實現的呢?——作用域鏈、閉包

父級函數FA()執行完成後就出棧銷燬了(典型場景就是返回函數)FB()可以到任何地方執行,那內部函數FB()執行的時候到哪裏去找父級函數的變量x呢?

  • ✅ 函數內部作用域:首先每個函數執行都會創建自己作用域(執行上下文),查找變量時優先本地作用域查找。
  • ✅ 閉包:引用的外部(詞法上級)函數作用域就形成了一個閉包,用一個Closure_(Closure /ˈkləʊʒə(r)/ 閉包)_對象保存,多個(外部引用)逐級保存到函數上下文的[[Scope]](Scope /skoʊp/ 作用域)集合上,形成作用域鏈
  • ✅ 作用域鏈的最底層就是指向全局對象的引用,她始終都在,不管你要不要她。
  • ✅ 變量查找就在這個作用域鏈上進行:自己上下文(詞法環境,變量環境) => 作用域鏈逐級查找=> 全局作用域 => undefined

image

function FA(x) {
    function FB(y) {
        x+=y;
        console.log(x);
    }
    console.dir(FB);
    return FB;  //返回FB()函數
}
let fb = FA(1);  //FA函數執行完成,出棧銷燬了
fb(2);  //3  //返回的fb()函數保留了他的父級FA()作用域變量x
fb(2);  //5	 //閉包中的x:我又變大了
fb(2);  //7  //同一個閉包函數重複調用,內部變量被改變

image.png

📢閉包簡單理解就是,當前環境中存放在指向父級作用域的引用。如果嵌套子函數完全沒有引用父級任何變量,就不會產生閉包。不過全局對象是始終存在其作用域鏈[[Scope]]上的。

🌰舉個例子

var a = 1;
let b = 2;
function FunA(x) {
    let x1 = 1;
    var x2 = 2;
    function FunB(y) {
        console.log(a + b + x + x1 + x2 + y);
    }
    FunB(2);
    console.dir(FunB)
}
FunA(1); //9
console.dir(FunA)

上面的代碼示例中,FunA()函數嵌套了FunB()函數,如下圖FunB()函數的[[Scope]]集合上有三個對象:

image.png

  • Closure (FunA) FunA()函數的閉包,包含他的參數x、私有變量x1x2
  • Script:Script Scope 腳本作用域(可以當做全局作用域的一部分),存放全局Script腳本環境內可訪問的letconst變量,就是全局作用域內的變量。var變量a被提升爲了全局對象window的“屬性”了。
  • Global:全局作用域對象,就是window,包含了var申明的變量,以及未申明的變量。

如果把FunB()函數放到外面申明,只在FunA()調用,其作用域鏈就不一樣了。


04、執行上下文是怎麼創建的?

執行上下文的創建過程中會創建對應的詞法作用域,包括詞法環境變量環境

  • 創建詞法環境(LexicalEnvironment):
    • 環境記錄EnvironmentRecord:記錄變量、函數的申明等信息,只存儲函數聲明和let/const聲明的變量。
    • 外層引用outer:對(上級)其他作用域詞法環境的引用,至少會包含全局上下文。
  • 創建變量環境(VariableEnvironment):本質上也是詞法環境,只不過他只存儲var申明的變量,其他都和詞法環境差不多。
ExecutionContext = {
    ThisBinding = <this value>,
    LexicalEnvironment = { ... },
    VariableEnvironment = { ... },
}

❗變量查找:變量查找的時候,是先從詞法環境中找,然後再到變量環境。就是優先查找const、let變量,其次才var變量。

image

換幾個角度來總結下,創建執行上下文主要搞定下面三個方面:

① 確定 this 的值(This Binding)

  • 在全局上下文中this指向window
  • 函數執行上下文中,如果它被一個對象引用調用,那麼 this 的值被設置爲該對象,否則 this 的值被設置爲全局對象或 undefined(嚴格模式下)
  • call(thisArg)、apply(thisArg)、bind(thisArg)會直接指定thisValue值。

② 內部環境:包括詞法環境變量環境,就是函數內部的變量、函數等信息,還有參數arguments信息。

③ 作用域鏈(外部引用):外部的詞法作用域存放到函數的[[Scope]]集合裏,用來查找上級作用域變量。


05、❓有什麼結論?

  • ❓ 變量越近越好:最好都本地化,儘量避免讓變量查找鏈路過長,一層層切換作用域去找也是很累的。
  • ❓ 優先const,其次let,儘量(堅決)不用var
  • ❓ 注意函數調用堆棧的長度,比如遞歸。
  • ❓ 閉包函數使用完後,手動釋放一下,fun = null;,儘早被垃圾回收。
  • ❓儘量避免成爲全局環境的變量,特別是一些臨時變量,全局對象始終都在,不會被垃圾回收。
    • 包括全局環境申明的的letconstvar
    • 切記不用未申明變量str='',不管在哪裏都會成爲全局變量。

遠離JavaScript、遠離前端......我以爲已經學會了,其實可能還沒入門。

image.png


10、GC內存管理

值類型變量的生命週期隨函數,函數執行完就釋放了。垃圾回收GC(Garbage Collection)內存管理主要針對引用對象,當檢測到對象不再會被使用,就釋放其內存。GC是自動運行的,不需干預也無法干預

GC回收一個對象的關鍵就是——確定他確是一個廢物,麼有任何地方使用他了,主要採用的方法就是標記清理。

  • 標記清理(mark-and-sweep):標記內存中的所有的可達對象和他所有引用的對象),剩下的就是沒人要的,可以刪除了。
  • 引用計數:按變量被引用的次數,這個策略已不再使用了,由於該回收垃圾的策略太垃圾從而被拋棄了。

❓什麼是可達性?

  • 🔸根(roots):當前執行環境(window)最直接的變量,包括當前執行函數的局部變量、參數;當前函數調用鏈上的其他函數的變量、參數;全局變量。
  • 🔸可達性(Reachability):如果一個值(對象)可以從根開始鏈式訪問到他,就是可達的,就說明這個數據對象還有利用價值。

image

上圖中FuncA函數中的局部變量 obj1,其值對象{P}存放在內存堆中,此時的值對象{P}被根變量obj1引用了,是可達的。

  • 如果函數執行完畢,函數就銷燬了,變量引用obj1也一起隨她而去。值對象{P}就沒有被引用了,就不可達了。
  • 如果在函數中顯示執行 obj1=null; 同樣的值對象{P}沒有被引用了,就不可達了。

image.png

GC定期執行垃圾回收的兩個步驟:

① 標記階段:找到可達對象並標記,實際的算法會更加精細。

  • 垃圾收集器找到所有的根,並“標記”(記住)它們。
  • 繼續遍歷並“標記”被根引用的對象。
  • ...繼續遍歷,直到找到所有可達對象並標記。

② 清除階段:沒有被標記的對象都會被清理刪除。

⚠️全局變量不會被清理:屬於window的全局變量就是根,始終不會被清理,有背景靠山就是不一樣!


©️版權申明:版權所有@安木夕,本文內容僅供學習,歡迎指正、交流,轉載請註明出處!原文編輯地址-語雀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章