搜尋
首頁web前端js教程前端進階(四):詳細圖解作用域鏈與閉包

前端進階(四):詳細圖解作用域鏈與閉包

攻克閉包難題


初學JavaScript的時候,我在學習閉包上,走了很多彎路。而這次重新回過頭來對基礎知識進​​行梳理,要講清楚閉包,也是一個非常大的挑戰。

閉包有多重要?如果你是初入前端的朋友,我沒有辦法直觀的告訴你閉包在實際開發中的無處不在,但是我可以告訴你,前端面試,必問閉包。面試官們常常用對閉包的了解程度來判定面試者的基礎水平,保守估計,10個前端面試者,至少5個都死在閉包上。

可是為什麼,閉包如此重要,還是有那麼多人沒有搞清楚呢?是因為大家不願意學習嗎?還真不是,而是我們透過搜尋找到的大部分講解閉包的中文文章,都沒有清晰明了的把閉包講解清楚。要嘛淺嚐輒止,要嘛高深莫測,要嘛乾脆就直接亂說一通。包括我自己曾經也寫過一篇關於閉包的總結,回頭一看,不忍直視[摀臉]。

因此本文的目的就在於,能夠清晰明了得把閉包說清楚,讓讀者老爺們看了之後,就把閉包給徹底學會了,而不是似懂非懂。

一、作用域與作用域鏈

在詳細講解作用域鏈之前,我預設你已經大概明白了JavaScript中的下面這些重要概念。這些概念將會非常有幫助。

如果暫時還沒明白,可以去看本系列的前三篇文章,本文文末有目錄連結。為了講解閉包,我已經為大家做好了基本的鋪墊。哈哈,真是好大一齣戲。

作用域

  • 在JavaScript中,我們可以將作用域定義為一套規則,這套規則用來管理引擎如何在在目前作用域以及嵌套的子作用域中根據識別符名稱進行變數查找。

    這裡的標識符,指的是

    變數名稱函數名稱

  • JavaScript中只有全域作用域與

    函數作用域(因為eval我們平時開發中幾乎不會用到它,這裡不討論)。

  • 作用域與執行上下文是完全不同的兩個概念。我知道很多人會混淆他們,但是一定要仔細區分。

    JavaScript程式碼的整個執行過程,分成兩個階段,程式碼編譯階段與程式碼執行階段。編譯階段由編譯器完成,將程式碼翻譯成可執行程式碼,這個階段作用域規則會決定。執行階段由引擎完成,主要任務是執行可執行程式碼,執行上下文在這個階段建立。

前端進階(四):詳細圖解作用域鏈與閉包

程式

#作用域鏈

回顧一下上一篇文章我們分析的執行上下文的

生命週期,如下圖。

前端進階(四):詳細圖解作用域鏈與閉包

執行上下文生命週期

我們發現,作用域鍊是在執行上下文的建立階段產生的。這個就奇怪了。上面我們剛剛說作用域在編譯階段確定規則,可是為什麼作用域鏈卻在執行階段確定呢?

之所以有這個疑問,是因為大家對作用域和作用域鏈有一個誤解。我們上面說了,作用域是一套規則,那麼作用域鍊是什麼呢?是這套規則的具體實作。所以這就是作用域與作用域鏈的關係,相信大家都應該明白了吧。

我們知道函數在呼叫啟動時,會開始建立對應的執行上下文,在執行上下文產生的過程中,變數對象,作用域鏈,以及this的值會分別被決定。之前一篇文章我們詳細說明了變數對象,而這裡,我們將詳細說明作用域鏈。

作用域鏈,是由當前環境與上層環境的一系列變數物件組成,它保證了目前執行環境對符合存取權限的變數和函數的有序存取。

為了幫助大家理解作用域鏈,我我們先結合一個例子,以及對應的圖示來說明。

var a = 20;

function test() {
    var b = a + 10;

    function innerTest() {
        var c = 10;
        return b + c;
    }

    return innerTest();
}

test();

在上面的範例中,全域,函數test,函數innerTest的執行上下文先後建立。我們設定他們的變數物件分別為VO(global),VO(test), VO(innerTest)。而innerTest的作用域鏈,則同時包含了這三個變數對象,所以innerTest的執行上下文可如下表示。

innerTestEC = {
    VO: {...},  // 变量对象
    scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
    this: {}
}

是的,你沒有看錯,我們可以直接用一個數組來表示作用域鏈,數組的第一項scopeChain[0]為作用域鏈的最前端,而陣列的最後一項,為作用域鏈的最末端,所有的最末端都是全域變數物件。

很多人會誤解為目前作用域與上層作用域為包含關係,但其實不是。以最前端為起點,最末端為終點的單方向通道我認為是更貼切的形容。如圖。

前端進階(四):詳細圖解作用域鏈與閉包

作用域鏈圖示

#注意,因為變數物件在執行上下文進入執行階段時,就變成了活動對象,這一點在上一篇文章中已經講過,因此圖中使用了AO來表示。 Active Object

是的,作用域鏈是由一系列變數物件組成,我們可以在這個單向通道中,查詢變數物件中的標識符,這樣就可以存取到上一層作用域中的變數了。

二、閉包

#對於那些有一點JavaScript 使用經驗但從未真正理解閉包概念的人來說,理解閉包可以看作是某種意義上的重生,突破閉包的瓶頸可以使你功力大增。

  • 閉包與作用域鏈息息相關;

  • #閉包是在函數執行過程中被確認。

先直截了當的拋出閉包的定義:當函數可以記住並存取所在的作用域(全域作用域除外)時,就產生了閉包,即使函數是在目前作用域之外執行。

簡單來說,假設函數A在函數B的內部進行定義了,並且當函數A在執行時,訪問了函數B內部的變數對象,那麼B就是一個閉包。

在基礎進階(一)中,我總結了JavaScript的垃圾回收機制。 JavaScript擁有自動的垃圾回收機制,關於垃圾回收機制,有一個重要的行為,那就是,當一個值,在記憶體中失去引用時,垃圾回收機制會根據特殊的演算法找到它,並將其回收,釋放記憶體。

而我們知道,函數的執行上下文,在執行完畢之後,生命週期結束,那麼該函數的執行上下文就會失去引用。其佔用的記憶體空間很快就會被垃圾回收器釋放。可是閉包的存在,會阻止這個過程。

先來一個簡單的例子。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar(); // 2

在上面的範例中,foo()執行完畢之後,按照常理,其執行環境生命週期會結束,所佔記憶體被垃圾收集器釋放。但是透過fn = innerFoo,函數innerFoo的引用被保留了下來,複製給了全域變數fn。這個行為,導致了foo的變數對象,也被保留了下來。於是,函數fn在函數bar內部執行時,依然可以存取這個被保留下來的變數物件。所以此刻仍然能夠存取到變數a的值。

這樣,我們就可以稱foo為閉包。

下圖展示了閉包foo的作用域鏈。

前端進階(四):詳細圖解作用域鏈與閉包

閉包foo的作用域鏈,圖中標題寫錯了,請無視

我們可以在chrome瀏覽器的開發者工具中查看這段程式碼運行時產生的函數呼叫棧與作用域鏈的生成情況。如下圖。

關於如何在chrome中觀察閉包,以及更多閉包的例子,請閱讀基礎系列(六)

前端進階(四):詳細圖解作用域鏈與閉包

#從圖中可以看出,chrome瀏覽器認為閉包是foo,而不是通常我們認為的innerFoo

在上面的图中,红色箭头所指的正是闭包。其中Call Stack为当前的函数调用栈,Scope为当前正在被执行的函数的作用域链,Local为当前的局部变量。

所以,通过闭包,我们可以在其他的执行上下文中,访问到函数的内部变量。比如在上面的例子中,我们在函数bar的执行环境中访问到了函数foo的a变量。个人认为,从应用层面,这是闭包最重要的特性。利用这个特性,我们可以实现很多有意思的东西。

不过读者老爷们需要注意的是,虽然例子中的闭包被保存在了全局变量中,但是闭包的作用域链并不会发生任何改变。在闭包中,能访问到的变量,仍然是作用域链上能够查询到的变量。

对上面的例子稍作修改,如果我们在函数bar中声明一个变量c,并在闭包fn中试图访问该变量,运行结果会抛出错误。

var fn = null;
function foo() {
    var a = 2;
    function innnerFoo() { 
        console.log(c); // 在这里,试图访问函数bar中的c变量,会抛出错误
        console.log(a);
    }
    fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}

function bar() {
    var c = 100;
    fn(); // 此处的保留的innerFoo的引用
}

foo();
bar();

关于这一点,很多同学把函数调用栈与作用域链没有分清楚,所以有的大神看了我关于介绍执行上下文的文章时就义正言辞的说我的例子有问题,而这些评论有很大的误导作用,为了帮助大家自己拥有能够辨别的能力,所以我写了基础(六),教大家如何在chrome中观察闭包,作用域链,this等。当然我也不敢100%保证我文中的例子就一定正确,所以教大家如何去辨认我认为才是最重要的。

闭包的应用场景

接下来,我们来总结下,闭包的常用场景。

我们知道setTimeout的第一个参数是一个函数,第二个参数则是延迟的时间。在下面例子中,

function fn() {
    console.log('this is test.')
}
var timer =  setTimeout(fn, 1000);
console.log(timer);

执行上面的代码,变量timer的值,会立即输出出来,表示setTimeout这个函数本身已经执行完毕了。但是一秒钟之后,fn才会被执行。这是为什么?

按道理来说,既然fn被作为参数传入了setTimeout中,那么fn将会被保存在setTimeout变量对象中,setTimeout执行完毕之后,它的变量对象也就不存在了。可是事实上并不是这样。至少在这一秒钟的事件里,它仍然是存在的。这正是因为闭包。

很显然,这是在函数的内部实现中,setTimeout通过特殊的方式,保留了fn的引用,让setTimeout的变量对象,并没有在其执行完毕后被垃圾收集器回收。因此setTimeout执行结束后一秒,我们任然能够执行fn函数。

  • 柯里化

在函数式编程中,利用闭包能够实现很多炫酷的功能,柯里化算是其中一种。关于柯里化,我会在以后详解函数式编程的时候仔细总结。

  • 模块

在我看来,模块是闭包最强大的一个应用场景。如果你是初学者,对于模块的了解可以暂时不用放在心上,因为理解模块需要更多的基础知识。但是如果你已经有了很多JavaScript的使用经验,在彻底了解了闭包之后,不妨借助本文介绍的作用域链与闭包的思路,重新理一理关于模块的知识。这对于我们理解各种各样的设计模式具有莫大的帮助。

(function () {
    var a = 10;
    var b = 20;

    function add(num1, num2) {
        var num1 = !!num1 ? num1 : a;
        var num2 = !!num2 ? num2 : b;

        return num1 + num2;
    }

    window.add = add;
})();

add(10, 20);

在上面的例子中,我使用函数自执行的方式,创建了一个模块。add是模块对外暴露的一个公共方法。而变量a,b被作为私有变量。在面向对象的开发中,我们常常需要考虑是将变量作为私有变量,还是放在构造函数中的this中,因此理解闭包,以及原型链是一个非常重要的事情。模块十分重要,因此我会在以后的文章专门介绍,这里就暂时不多说啦。

前端進階(四):詳細圖解作用域鏈與閉包

此图中可以观看到当代码执行到add方法时的调用栈与作用域链,此刻的闭包为外层的自执行函数

为了验证自己有没有搞懂作用域链与闭包,这里留下一个经典的思考题,常常也会在面试中被问到。

利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5

for (var i=1; i<p>点此查看关于此题的详细解读</p><p>关于作用域链的与闭包我就总结完了,虽然我自认为我是说得非常清晰了,但是我知道理解闭包并不是一件简单的事情,所以如果你有什么问题,可以在评论中问我。你也可以带着从别的地方没有看懂的例子在评论中留言。大家一起学习进步。</p>

以上是前端進階(四):詳細圖解作用域鏈與閉包的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JavaScript在行動中:現實世界中的示例和項目JavaScript在行動中:現實世界中的示例和項目Apr 19, 2025 am 12:13 AM

JavaScript在現實世界中的應用包括前端和後端開發。 1)通過構建TODO列表應用展示前端應用,涉及DOM操作和事件處理。 2)通過Node.js和Express構建RESTfulAPI展示後端應用。

JavaScript和Web:核心功能和用例JavaScript和Web:核心功能和用例Apr 18, 2025 am 12:19 AM

JavaScript在Web開發中的主要用途包括客戶端交互、表單驗證和異步通信。 1)通過DOM操作實現動態內容更新和用戶交互;2)在用戶提交數據前進行客戶端驗證,提高用戶體驗;3)通過AJAX技術實現與服務器的無刷新通信。

了解JavaScript引擎:實施詳細信息了解JavaScript引擎:實施詳細信息Apr 17, 2025 am 12:05 AM

理解JavaScript引擎內部工作原理對開發者重要,因為它能幫助編寫更高效的代碼並理解性能瓶頸和優化策略。 1)引擎的工作流程包括解析、編譯和執行三個階段;2)執行過程中,引擎會進行動態優化,如內聯緩存和隱藏類;3)最佳實踐包括避免全局變量、優化循環、使用const和let,以及避免過度使用閉包。

Python vs. JavaScript:學習曲線和易用性Python vs. JavaScript:學習曲線和易用性Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

Python vs. JavaScript:社區,圖書館和資源Python vs. JavaScript:社區,圖書館和資源Apr 15, 2025 am 12:16 AM

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

從C/C到JavaScript:所有工作方式從C/C到JavaScript:所有工作方式Apr 14, 2025 am 12:05 AM

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境