在前端開發中,有一個很是重要的技能,叫作斷點調試。javascript
在chrome的開發者工具中,經過斷點調試,咱們可以很是方便的一步一步的觀察JavaScript的執行過程,直觀感知函數調用棧,做用域鏈,變量對象,閉包,this等關鍵信息的變化。所以,斷點調試對於快速定位代碼錯誤,快速瞭解代碼的執行過程有着很是重要的做用,這也是咱們前端開發者必不可少的一個高級技能。html
固然若是你對JavaScript的這些基礎概念(執行上下文,變量對象,閉包,this等)瞭解還不夠的話,想要透徹掌握斷點調試可能會有一些困難。可是好在在前面幾篇文章,我都對這些概念進行了詳細的概述,所以要掌握這個技能,對你們來講,應該是比較輕鬆的。前端
這篇文章的主要目的在於藉助對於斷點調試的學習,來進一步加深對閉包的理解。java
函數在被調用執行時,會建立一個當前函數的執行上下文。在該執行上下文的建立階段,變量對象、做用域鏈、閉包、this指向會分別被肯定。而一個JavaScript程序中通常來講會有多個函數,JavaScript引擎使用函數調用棧來管理這些函數的調用順序。函數調用棧的調用順序與棧數據結構一致。chrome
在儘可能新版本的chrome瀏覽器中(不肯定你用的老版本與個人一致),調出chrome瀏覽器的開發者工具。編程
瀏覽器右上角豎着的三點 -> 更多工具 -> 開發者工具 -> Sources
界面如圖。segmentfault
在個人demo中,我把代碼放在app.js中,在index.html中引入。咱們暫時只須要關注截圖中紅色箭頭的地方。在最右側上方,有一排圖標。咱們能夠經過使用他們來控制函數的執行順序。從左到右他們依次是:瀏覽器
跨過,實際表現是不遇到函數時,執行下一步。遇到函數時,不進入函數直接執行下一步。數據結構
跨入,實際表現是不遇到函數時,執行下一步。遇到到函數時,進入函數執行上下文。閉包
跳出當前函數
停用斷點
不暫停異常捕獲
其中跨過,跨入,跳出是我使用最多的三個操做。
上圖右側第二個紅色箭頭指向的是函數調用棧(call Stack),這裏會顯示代碼執行過程當中,調用棧的變化。
右側第三個紅色箭頭指向的是做用域鏈(Scope),這裏會顯示當前函數的做用域鏈。其中Local表示當前的局部變量對象,Closure表示當前做用域鏈中的閉包。藉助此處的做用域鏈展現,咱們能夠很直觀的判斷出一個例子中,到底誰是閉包,對於閉包的深刻了解具備很是重要的幫助做用。
在顯示代碼行數的地方點擊,便可設置一個斷點。斷點設置有如下幾個特色:
接下來,咱們藉助一些實例,來使用斷點調試工具,看一看,咱們的demo函數,在執行過程當中的具體表現。
// demo01 var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; } function bar() { fn(); } foo(); bar(); // 2
在向下閱讀以前,咱們能夠停下來思考一下,這個例子中,誰是閉包?
這是來自《你不知道的js》中的一個例子。因爲在使用斷點調試過程當中,發現chrome瀏覽器理解的閉包與該例子中所理解的閉包不太一致,所以專門挑出來,供你們參考。我我的更加傾向於chrome中的理解。
一步一步執行,當函數執行到上例子中
咱們能夠看到,在chrome工具的理解中,因爲在foo內部聲明的baz函數在調用時訪問了它的變量a,所以foo成爲了閉包。這好像和咱們學習到的知識不太同樣。咱們來看看在《你不知道的js》這本書中的例子中的理解。
書中的註釋能夠明顯的看出,做者認爲fn爲閉包。即baz,這和chrome工具中明顯是不同的。
而在備受你們推崇的《JavaScript高級編程》一書中,是這樣定義閉包。
這裏chrome中理解的閉包,與我所閱讀的這幾本書中的理解的閉包不同。其實在以前對於閉包分析的文章中,我已經有對這種狀況作了一個解讀。閉包詳解
閉包是一個特殊對象,它由執行上下文(代號A)與在該執行上下文中建立的函數(代號B)共同組成。
當B執行時,若是訪問了A中變量對象中的值,那麼閉包就會產生。
那麼在大多數理解中,包括許多著名的書籍,文章裏都以函數B的名字代指這裏生成的閉包。而在chrome中,則以執行上下文A的函數名代指閉包。
咱們修改一下demo01中的例子,來看看一個很是有意思的變化。
// demo02 var fn; var m = 20; function foo() { var a = 2; function baz(a) { console.log(a); } fn = baz; } function bar() { fn(m); } foo(); bar(); // 20
這個例子在demo01的基礎上,我在baz函數中傳入一個參數,並打印出來。在調用時,我將全局的變量m傳入。輸出結果變爲20。在使用斷點調試看看做用域鏈。
是否是結果有點意外,閉包沒了,做用域鏈中沒有包含foo了。我靠,跟咱們理解的好像又有點不同。因此經過這個對比,咱們能夠肯定閉包的造成須要兩個條件。
還有更有意思的。
咱們繼續來看看一個例子。
// demo03 function foo() { var a = 2; return function bar() { var b = 9; return function fn() { console.log(a); } } } var bar = foo(); var fn = bar(); fn();
在這個例子中,fn只訪問了foo中的a變量,所以它的閉包只有foo。
修改一下demo03,咱們在fn中也訪問bar中b變量試試看。
// demo04 function foo() { var a = 2; return function bar() { var b = 9; return function fn() { console.log(a, b); } } } var bar = foo(); var fn = bar(); fn();
這個時候,閉包變成了兩個。分別是bar,foo。
咱們知道,閉包在模塊中的應用很是重要。所以,咱們來一個模塊的例子,也用斷點工具來觀察一下。
// demo05 (function() { var a = 10; var b = 20; var test = { m: 20, add: function(x) { return a + x; }, sum: function() { return a + b + this.m; }, mark: function(k, j) { return k + j; } } window.test = test; })(); test.add(100); test.sum(); test.mark(); var _mark = test.mark; _mark();
注意:這裏的this指向顯示爲Object或者Window,大寫開頭,他們表示的是實例的構造函數,實際上this是指向的具體實例test.mark能造成閉包,跟下面的補充例子(demo07)狀況是同樣的。
咱們還能夠結合點斷調試的方式,來理解那些困擾咱們好久的this指向。隨時觀察this的指向,在實際開發調試中很是有用。
// demo06 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn.call(obj); // 20
最後繼續補充一個例子。
// demo07 function foo() { var a = 10; function fn1() { return a; } function fn2() { return 10; } fn2(); } foo();
這個例子,和其餘例子不太同樣。雖然fn2並無訪問到foo的變量,可是foo執行時仍然變成了閉包。而當我將fn1的聲明去掉時,閉包便不會出現了。
那麼結合這個特殊的例子,咱們能夠這樣這樣定義閉包。
閉包是指這樣的做用域(foo),它包含有一個函數(fn1),這個函數(fn1)能夠調用被這個做用域所封閉的變量(a)、函數、或者閉包等內容。一般咱們經過閉包所對應的函數來得到對閉包的訪問。
更多的例子,你們能夠自行嘗試,總之,學會了使用斷點調試以後,咱們就可以很輕鬆的瞭解一段代碼的執行過程了。這對快速定位錯誤,快速瞭解他人的代碼都有很是巨大的幫助。你們必定要動手實踐,把它給學會。
最後,根據以上的摸索狀況,再次總結一下閉包:
你們也能夠根據我提供的這個方法,對其餘的例子進行更多的測試,若是發現個人結論有不對的地方,歡迎指出,你們相互學習進步,謝謝你們。