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

配圖與本文無關

在前端開發中,有一個很是重要的技能,叫作斷點調試javascript

在chrome的開發者工具中,經過斷點調試,咱們可以很是方便的一步一步的觀察JavaScript的執行過程,直觀感知函數調用棧,做用域鏈,變量對象,閉包,this等關鍵信息的變化。所以,斷點調試對於快速定位代碼錯誤,快速瞭解代碼的執行過程有着很是重要的做用,這也是咱們前端開發者必不可少的一個高級技能。html

固然若是你對JavaScript的這些基礎概念(執行上下文,變量對象,閉包,this等)瞭解還不夠的話,想要透徹掌握斷點調試可能會有一些困難。可是好在在前面幾篇文章,我都對這些概念進行了詳細的概述,所以要掌握這個技能,對你們來講,應該是比較輕鬆的。前端

這篇文章的主要目的在於藉助對於斷點調試的學習,來進一步加深對閉包的理解。java

1、基礎概念回顧

函數在被調用執行時,會建立一個當前函數的執行上下文。在該執行上下文的建立階段,變量對象、做用域鏈、閉包、this指向會分別被肯定。而一個JavaScript程序中通常來講會有多個函數,JavaScript引擎使用函數調用棧來管理這些函數的調用順序。函數調用棧的調用順序與棧數據結構一致。chrome

2、認識斷點調試工具

在儘可能新版本的chrome瀏覽器中(不肯定你用的老版本與個人一致),調出chrome瀏覽器的開發者工具。編程

瀏覽器右上角豎着的三點 -> 更多工具 -> 開發者工具 -> Sources

界面如圖。segmentfault

斷點調試界面

在個人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表示當前做用域鏈中的閉包。藉助此處的做用域鏈展現,咱們能夠很直觀的判斷出一個例子中,到底誰是閉包,對於閉包的深刻了解具備很是重要的幫助做用。

3、斷點設置

在顯示代碼行數的地方點擊,便可設置一個斷點。斷點設置有如下幾個特色:

  • 在單獨的變量聲明(若是沒有賦值),函數聲明的那一行,沒法設置斷點。
  • 設置斷點後刷新頁面,JavaScript代碼會執行到斷點位置處暫停執行,而後咱們就可使用上邊介紹過的幾個操做開始調試了。
  • 當你設置多個斷點時,chrome工具會自動判斷從最先執行的那個斷點開始執行,所以我通常都是設置一個斷點就好了。
4、實例

接下來,咱們藉助一些實例,來使用斷點調試工具,看一看,咱們的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中理解的閉包,與我所閱讀的這幾本書中的理解的閉包不同。其實在以前對於閉包分析的文章中,我已經有對這種狀況作了一個解讀。閉包詳解

閉包是一個特殊對象,它由執行上下文(代號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了。

是否是結果有點意外,閉包沒了,做用域鏈中沒有包含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能造成閉包,跟下面的補充例子(demo07)狀況是同樣的。

咱們還能夠結合點斷調試的方式,來理解那些困擾咱們好久的this指向。隨時觀察this的指向,在實際開發調試中很是有用。

// demo06

var a = 10;
var obj = {
    a: 20
}

function fn () {
    console.log(this.a);
}

fn.call(obj); // 20

this指向obj

最後繼續補充一個例子。

// demo07
function foo() {
    var a = 10;

    function fn1() {
        return a;
    }

    function fn2() {
        return 10;
    }

    fn2();
}

foo();

這個例子,和其餘例子不太同樣。雖然fn2並無訪問到foo的變量,可是foo執行時仍然變成了閉包。而當我將fn1的聲明去掉時,閉包便不會出現了。

那麼結合這個特殊的例子,咱們能夠這樣這樣定義閉包。

閉包是指這樣的做用域(foo),它包含有一個函數(fn1),這個函數(fn1)能夠調用被這個做用域所封閉的變量(a)、函數、或者閉包等內容。一般咱們經過閉包所對應的函數來得到對閉包的訪問。

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

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

  • 閉包是在函數被調用執行的時候才被確認建立的。
  • 閉包的造成,與做用域鏈的訪問順序有直接關係。
  • 只有內部函數訪問了上層做用域鏈中的變量對象時,纔會造成閉包,所以,咱們能夠利用閉包來訪問函數內部的變量。
你們也能夠根據我提供的這個方法,對其餘的例子進行更多的測試,若是發現個人結論有不對的地方,歡迎指出,你們相互學習進步,謝謝你們。

前端基礎進階系列目錄

clipboard.png

相關文章
相關標籤/搜索