前端高手必備技能:如何在 chrome 開發者工具中觀察函數調用棧、作用域鏈與閉包

454771942 7年前發布 | 17K 次閱讀 閉包 前端技術

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

在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

 

 本文由用戶 454771942 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!