前端高手必備技能:如何在 chrome 開發者工具中觀察函數調用棧、作用域鏈與閉包
在前端開發中,有一個非常重要的技能,叫做 斷點調試 。
在chrome的開發者工具中,通過斷點調試,我們能夠非常方便的一步一步的觀察JavaScript的執行過程,直觀感知函數調用棧,作用域鏈,變量對象,閉包,this等關鍵信息的變化。因此,斷點調試對于快速定位代碼錯誤,快速了解代碼的執行過程有著非常重要的作用,這也是我們前端開發者必不可少的一個高級技能。
當然如果你對JavaScript的這些基礎概念[執行上下文,變量對象,閉包,this等]了解還不夠的話,想要透徹掌握斷點調試可能會有一些困難。但是好在在前面幾篇文章,我都對這些概念進行了詳細的概述,因此要掌握這個技能,對大家來說,應該是比較輕松的。
為了幫助大家對于this與閉包有更好的了解,也因為上一篇文章里對閉包的定義有一點偏差,因此這篇文章里我就以閉包有關的例子來進行斷點調試的學習,以便大家及時糾正。在這里認個錯,誤導大家了,求輕噴 ~ ~
一、基礎概念回顧
函數在被調用執行時,會創建一個當前函數的執行上下文。在該執行上下文的創建階段,變量對象、作用域鏈、閉包、this指向會分別被確定。而一個JavaScript程序中一般來說會有多個函數,JavaScript引擎使用函數調用棧來管理這些函數的調用順序。函數調用棧的調用順序與棧數據結構一致。
二、認識斷點調試工具
在盡量新版本的chrome瀏覽器中(不確定你用的老版本與我的一致),調出chrome瀏覽器的開發者工具。
瀏覽器右上角豎著的三點 -> 更多工具 -> 開發者工具 -> Sources
界面如圖。
斷點調試界面
在我的demo中,我把代碼放在app.js中,在index.html中引入。我們暫時只需要關注截圖中紅色箭頭的地方。在最左側上方,有一排圖標。我們可以通過使用他們來控制函數的執行順序。從左到右他們依次是:
-
resume/pause script execution
恢復/暫停腳本執行
-
step over next function call
跨過,實際表現是不遇到函數時,執行下一步。遇到函數時,不進入函數直接執行下一步。
-
step into next function call
跨入,實際表現是不遇到函數時,執行下一步。遇到到函數時,進入函數執行上下文。
-
step out of current function
跳出當前函數
-
deactivate breakpoints
停用斷點
-
don‘t pause on exceptions
不暫停異常捕獲
其中跨過,跨入,跳出是我使用最多的三個操作。
上圖左側第二個紅色箭頭指向的是函數調用棧(call Stack),這里會顯示代碼執行過程中,調用棧的變化。
左側第三個紅色箭頭指向的是作用域鏈(Scope),這里會顯示當前函數的作用域鏈。其中Local表示當前的局部變量對象,Closure表示當前作用域鏈中的閉包。借助此處的作用域鏈展示,我們可以很直觀的判斷出一個例子中,到底誰是閉包,對于閉包的深入了解具有非常重要的幫助作用。
三、斷點設置
在顯示代碼行數的地方點擊,即可設置一個斷點。斷點設置有以下幾個特點:
-
在單獨的變量聲明(如果沒有賦值),函數聲明的那一行,無法設置斷點。
-
設置斷點后刷新頁面,JavaScript代碼會執行到斷點位置處暫停執行,然后我們就可以使用上邊介紹過的幾個操作開始調試了。
-
當你設置多個斷點時,chrome工具會自動判斷從最早執行的那個斷點開始執行,因此我一般都是設置一個斷點就行了。
四、實例
接下來,我們借助一些實例,來使用斷點調試工具,看一看,我們的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中的理解。
- 第一步:設置斷點,然后刷新頁面。
設置斷點
- 第二步:點擊上圖紅色箭頭指向的按鈕(step into),該按鈕的作用會根據代碼執行順序,一步一步向下執行。在點擊的過程中,我們要注意觀察下方call stack 與 scope的變化,以及函數執行位置的變化。
一步一步執行,當函數執行到上例子中
baz函數被調用執行,foo形成了閉包
我們可以看到,在chrome工具的理解中,由于在foo內部聲明的baz函數在調用時訪問了它的變量a,因此foo成為了閉包。這好像和我們學習到的知識不太一樣。我們來看看在《你不知道的js》這本書中的例子中的理解。
你不知道的js中的例子
書中的注釋可以明顯的看出,作者認為fn為閉包。即baz,這和chrome工具中明顯是不一樣的。
而在備受大家推崇的《JavaScript高級編程》一書中,是這樣定義閉包。
JavaScript高級編程中閉包的定義
書中作者將自己理解的閉包與包含函數所區分
這里chrome中理解的閉包,與我所閱讀的這幾本書中的理解的閉包不一樣。具體這里我先不下結論,但是我心中更加偏向于相信chrome瀏覽器。
我們修改一下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了。
是不是結果有點意外,閉包沒了,作用域鏈中沒有包含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。
閉包只有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();
add執行時,閉包為外層的自執行函數,this指向test
sum執行時,同上
mark執行時,閉包為外層的自執行函數,this指向test
_mark執行時,閉包為外層的自執行函數,this指向window
注意:這里的this指向顯示為Object或者Window,大寫開頭,他們表示的是實例的構造函數,實際上this是指向的具體實例
上面的所有調用,最少都訪問了自執行函數中的test變量,因此都能形成閉包。即使mark方法沒有訪問私有變量a,b。
我們還可以結合點斷調試的方式,來理解那些困擾我們很久的this指向。隨時觀察this的指向,在實際開發調試中非常有用。
// demo06 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn.call(obj); // 20
this指向obj
更多的例子,大家可以自行嘗試,總之,學會了使用斷點調試之后,我們就能夠很輕松的了解一段代碼的執行過程了。這對快速定位錯誤,快速了解他人的代碼都有非常巨大的幫助。大家一定要動手實踐,把它給學會。
最后,根據以上的摸索情況,再次總結一下閉包:
-
閉包是在函數被調用執行的時候才被確認創建的。
-
閉包的形成,與作用域鏈的訪問順序有直接關系。
-
只有內部函數訪問了上層作用域鏈中的變量對象時,才會形成閉包,因此,我們可以利用閉包來訪問函數內部的變量。
-
chrome中理解的閉包,與《你不知道的js》與《JavaScript高級編程》中的閉包理解有很大不同,我個人更加傾向于相信chrome。這里就不妄下結論了,大家可以根據我的思路,探索后自行確認。在之前一篇文中我根據從書中學到的下了定義,應該是錯了,目前已經修改,對不起大家了。
大家也可以根據我提供的這個方法,對其他的例子進行更多的測試,如果發現我的結論有不對的地方,歡迎指出,大家相互學習進步,謝謝大家。感覺這個問題,很嚴重,可能國內很多人,都對閉包的理解出了錯 - -!需要大家集體的力量,來下一個定義。
來自:http://www.jianshu.com/p/73122bb3d262