前端基礎進階(六):在chrome開發者工具中觀察函式呼叫棧、作用域鏈與閉包

發表於2017-02-26

599584-64a8168e053e721f

配圖與本文無關

在前端開發中,有一個非常重要的技能,叫做斷點除錯

在chrome的開發者工具中,通過斷點除錯,我們能夠非常方便的一步一步的觀察JavaScript的執行過程,直觀感知函式呼叫棧,作用域鏈,變數物件,閉包,this等關鍵資訊的變化。因此,斷點除錯對於快速定位程式碼錯誤,快速瞭解程式碼的執行過程有著非常重要的作用,這也是我們前端開發者必不可少的一個高階技能。

當然如果你對JavaScript的這些基礎概念[執行上下文,變數物件,閉包,this等]瞭解還不夠的話,想要透徹掌握斷點除錯可能會有一些困難。但是好在在前面幾篇文章,我都對這些概念進行了詳細的概述,因此要掌握這個技能,對大家來說,應該是比較輕鬆的。

為了幫助大家對於this與閉包有更好的瞭解,也因為上一篇文章裡對閉包的定義有一點偏差,因此這篇文章裡我就以閉包有關的例子來進行斷點除錯的學習,以便大家及時糾正。在這裡認個錯,誤導大家了,求輕噴 ~ ~

一、基礎概念回顧

函式在被呼叫執行時,會建立一個當前函式的執行上下文。在該執行上下文的建立階段,變數物件、作用域鏈、閉包、this指向會分別被確定。而一個JavaScript程式中一般來說會有多個函式,JavaScript引擎使用函式呼叫棧來管理這些函式的呼叫順序。函式呼叫棧的呼叫順序與棧資料結構一致。

二、認識斷點除錯工具

在儘量新版本的chrome瀏覽器中(不確定你用的老版本與我的一致),調出chrome瀏覽器的開發者工具。

介面如圖。

599584-56f0737789bb3c36

斷點除錯介面

在我的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函式,在執行過程中的具體表現。

在向下閱讀之前,我們可以停下來思考一下,這個例子中,誰是閉包?

這是來自《你不知道的js》中的一個例子。由於在使用斷點除錯過程中,發現chrome瀏覽器理解的閉包與該例子中所理解的閉包不太一致,因此專門挑出來,供大家參考。我個人更加傾向於chrome中的理解。

  • 第一步:設定斷點,然後重新整理頁面。

599584-ed677cf1c64e39e7

設定斷點
  • 第二步:點選上圖紅色箭頭指向的按鈕(step into),該按鈕的作用會根據程式碼執行順序,一步一步向下執行。在點選的過程中,我們要注意觀察下方call stack 與 scope的變化,以及函式執行位置的變化。

一步一步執行,當函式執行到上例子中

599584-1b8e8f6a6cee5b88

baz函式被呼叫執行,foo形成了閉包

我們可以看到,在chrome工具的理解中,由於在foo內部宣告的baz函式在呼叫時訪問了它的變數a,因此foo成為了閉包。這好像和我們學習到的知識不太一樣。我們來看看在《你不知道的js》這本書中的例子中的理解。

599584-7a72e8a1b8fdd764

你不知道的js中的例子

書中的註釋可以明顯的看出,作者認為fn為閉包。即baz,這和chrome工具中明顯是不一樣的。

而在備受大家推崇的《JavaScript高階程式設計》一書中,是這樣定義閉包。

599584-b30c0aee7668c183

JavaScript高階程式設計中閉包的定義

599584-ee0c3051f02ec5d8

書中作者將自己理解的閉包與包含函式所區分

這裡chrome中理解的閉包,與我所閱讀的這幾本書中的理解的閉包不一樣。具體這裡我先不下結論,但是我心中更加偏向於相信chrome瀏覽器。

我們修改一下demo01中的例子,來看看一個非常有意思的變化。

這個例子在demo01的基礎上,我在baz函式中傳入一個引數,並列印出來。在呼叫時,我將全域性的變數m傳入。輸出結果變為20。在使用斷點除錯看看作用域鏈。

599584-f74b68b5f041ca9e

閉包沒了,作用域鏈中沒有包含foo了。

是不是結果有點意外,閉包沒了,作用域鏈中沒有包含foo了。我靠,跟我們理解的好像又有點不一樣。所以通過這個對比,我們可以確定閉包的形成需要兩個條件。

  • 在函式內部建立新的函式;
  • 新的函式在執行時,訪問了函式的變數物件;

還有更有意思的。

我們繼續來看看一個例子。

在這個例子中,fn只訪問了foo中的a變數,因此它的閉包只有foo。

599584-6e98041bfd2f719f

閉包只有foo

修改一下demo03,我們在fn中也訪問bar中b變數試試看。

599584-431d16611cac1243

這個時候閉包變成了兩個

這個時候,閉包變成了兩個。分別是bar,foo。

我們知道,閉包在模組中的應用非常重要。因此,我們來一個模組的例子,也用斷點工具來觀察一下。

599584-662ec3ce1cf33206

add執行時,閉包為外層的自執行函式,this指向test

599584-24572d8b5dd381b6

sum執行時,同上

599584-77888095edb980a7

mark執行時,閉包為外層的自執行函式,this指向test

599584-fedeee99354936a9

_mark執行時,閉包為外層的自執行函式,this指向window

注意:這裡的this指向顯示為Object或者Window,大寫開頭,他們表示的是例項的建構函式,實際上this是指向的具體例項

上面的所有呼叫,最少都訪問了自執行函式中的test變數,因此都能形成閉包。即使mark方法沒有訪問私有變數a,b。

我們還可以結合點斷除錯的方式,來理解那些困擾我們很久的this指向。隨時觀察this的指向,在實際開發除錯中非常有用。

599584-ab511b394be82692

this指向obj

更多的例子,大家可以自行嘗試,總之,學會了使用斷點除錯之後,我們就能夠很輕鬆的瞭解一段程式碼的執行過程了。這對快速定位錯誤,快速瞭解他人的程式碼都有非常巨大的幫助。大家一定要動手實踐,把它給學會。

最後,根據以上的摸索情況,再次總結一下閉包:

  • 閉包是在函式被呼叫執行的時候才被確認建立的。
  • 閉包的形成,與作用域鏈的訪問順序有直接關係。
  • 只有內部函式訪問了上層作用域鏈中的變數物件時,才會形成閉包,因此,我們可以利用閉包來訪問函式內部的變數。
  • chrome中理解的閉包,與《你不知道的js》與《JavaScript高階程式設計》中的閉包理解有很大不同,我個人更加傾向於相信chrome。這裡就不妄下結論了,大家可以根據我的思路,探索後自行確認。在之前一篇文中我根據從書中學到的下了定義,應該是錯了,目前已經修改,對不起大家了。

大家也可以根據我提供的這個方法,對其他的例子進行更多的測試,如果發現我的結論有不對的地方,歡迎指出,大家相互學習進步,謝謝大家。

相關文章