在前端開發中,有一個非常重要的技能,叫做斷點除錯。
在chrome的開發者工具中,通過斷點除錯,我們能夠非常方便的一步一步的觀察JavaScript的執行過程,直觀感知函式呼叫棧,作用域鏈,變數物件,閉包,this等關鍵資訊的變化。因此,斷點除錯對於快速定位程式碼錯誤,快速瞭解程式碼的執行過程有著非常重要的作用,這也是我們前端開發者必不可少的一個高階技能。
當然如果你對JavaScript的這些基礎概念[執行上下文,變數物件,閉包,this等]瞭解還不夠的話,想要透徹掌握斷點除錯可能會有一些困難。但是好在在前面幾篇文章,我都對這些概念進行了詳細的概述,因此要掌握這個技能,對大家來說,應該是比較輕鬆的。
為了幫助大家對於this與閉包有更好的瞭解,也因為上一篇文章裡對閉包的定義有一點偏差,因此這篇文章裡我就以閉包有關的例子來進行斷點除錯的學習,以便大家及時糾正。在這裡認個錯,誤導大家了,求輕噴 ~ ~
一、基礎概念回顧
函式在被呼叫執行時,會建立一個當前函式的執行上下文。在該執行上下文的建立階段,變數物件、作用域鏈、閉包、this指向會分別被確定。而一個JavaScript程式中一般來說會有多個函式,JavaScript引擎使用函式呼叫棧來管理這些函式的呼叫順序。函式呼叫棧的呼叫順序與棧資料結構一致。
二、認識斷點除錯工具
在儘量新版本的chrome瀏覽器中(不確定你用的老版本與我的一致),調出chrome瀏覽器的開發者工具。
1 |
瀏覽器右上角豎著的三點 -> 更多工具 -> 開發者工具 -> 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函式,在執行過程中的具體表現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 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的變化,以及函式執行位置的變化。
一步一步執行,當函式執行到上例子中
我們可以看到,在chrome工具的理解中,由於在foo內部宣告的baz函式在呼叫時訪問了它的變數a,因此foo成為了閉包。這好像和我們學習到的知識不太一樣。我們來看看在《你不知道的js》這本書中的例子中的理解。
書中的註釋可以明顯的看出,作者認為fn為閉包。即baz,這和chrome工具中明顯是不一樣的。
而在備受大家推崇的《JavaScript高階程式設計》一書中,是這樣定義閉包。
這裡chrome中理解的閉包,與我所閱讀的這幾本書中的理解的閉包不一樣。具體這裡我先不下結論,但是我心中更加偏向於相信chrome瀏覽器。
我們修改一下demo01中的例子,來看看一個非常有意思的變化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 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了。我靠,跟我們理解的好像又有點不一樣。所以通過這個對比,我們可以確定閉包的形成需要兩個條件。
- 在函式內部建立新的函式;
- 新的函式在執行時,訪問了函式的變數物件;
還有更有意思的。
我們繼續來看看一個例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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變數試試看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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。
我們知道,閉包在模組中的應用非常重要。因此,我們來一個模組的例子,也用斷點工具來觀察一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 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方法沒有訪問私有變數a,b。
我們還可以結合點斷除錯的方式,來理解那些困擾我們很久的this指向。隨時觀察this的指向,在實際開發除錯中非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 |
// demo06 var a = 10; var obj = { a: 20 } function fn () { console.log(this.a); } fn.call(obj); // 20 |
更多的例子,大家可以自行嘗試,總之,學會了使用斷點除錯之後,我們就能夠很輕鬆的瞭解一段程式碼的執行過程了。這對快速定位錯誤,快速瞭解他人的程式碼都有非常巨大的幫助。大家一定要動手實踐,把它給學會。
最後,根據以上的摸索情況,再次總結一下閉包:
- 閉包是在函式被呼叫執行的時候才被確認建立的。
- 閉包的形成,與作用域鏈的訪問順序有直接關係。
- 只有內部函式訪問了上層作用域鏈中的變數物件時,才會形成閉包,因此,我們可以利用閉包來訪問函式內部的變數。
- chrome中理解的閉包,與《你不知道的js》與《JavaScript高階程式設計》中的閉包理解有很大不同,我個人更加傾向於相信chrome。這裡就不妄下結論了,大家可以根據我的思路,探索後自行確認。在之前一篇文中我根據從書中學到的下了定義,應該是錯了,目前已經修改,對不起大家了。
大家也可以根據我提供的這個方法,對其他的例子進行更多的測試,如果發現我的結論有不對的地方,歡迎指出,大家相互學習進步,謝謝大家。