一道js閉包面試題的學習

最近看到一條有意思的閉包面試題,可是看到原文的解析,我本身以爲有點迷糊,因此本身從新作一下這條題目。html

閉包面試題原題

function fun(n, o) { // ① 
  console.log(o);
  return { // ② 
    fun: function(m) { // ③ 
      return fun(m, n); // ④ 
    }
  };
}

// 第一個例子
var a = fun(0); // 返回undefined
a.fun(1); // 返回 ?
a.fun(2); // 返回 ?
a.fun(3); // 返回 ?

// 第二個例子
var b = fun(0)
  .fun(1)
  .fun(2)
  .fun(3); //undefined,?,?,?

// 第三個例子
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); //undefined,?,?,?

1、關於這個函數的執行過程

先大體說一下這個函數的執行過程:面試

① 初始化一個具名函數,具名函數就是有名字的函數,名字叫 fun。閉包

② 第一個 fun 具名函數執行以後會返回一個對象字面量表達式,即返回一個新的object對象。函數

{  // 這是一個對象,這是對象字面量表達式建立對象的寫法,例如{a:11,b:22}
  fun: function(m) { 
    return fun(m, n); 
  }
}

③ 返回的對象裏面含有fun這個屬性,而且這個屬性裏面存放的是一個新建立匿名函數表達式function(m) {}code

④ 在③裏面建立的匿名函數會返回一個叫 fun 的具名函數return fun(m, n);,這裏須要說明一下這個 fun 函數返回以後的執行過程:htm

1. 返回 fun 函數,但默認不執行,由於在 js 裏面,函數是能夠保存在變量裏面的。

2. 若是想要執行 fun 函數,那麼首先會在當前做用域尋找叫fun 名字的具名函數,可是由於當前做用域裏 fun 名字的函數是沒有被定義的,因此會自動往上一級查找。
    2.1 註解:當前的做用域裏是一個新建立的對象,而且對象裏面只有 fun 屬性,而沒有 fun 具名函數
    2.2 註解:js 做用域鏈的問題,會致使他會不斷地往上級鏈查找。

3. 在當前做用域沒找到,因此一直往上層找,直到找到了頂層的 fun函數,而後執行這個頂層的 fun 函數。

4. 而後這兩個 fun 函數會造成閉包,第二個 fun 函數會不斷引用第一個 fun 函數,從而致使一些局部變量例如 n,o 得以保存。
所謂閉包:各類解釋都有,但都不是很接地氣,簡單的來講就是在 js 裏面,有一些變量(內存)能夠被不斷的引用,致使了變量(內存)沒有被釋放和回收,從而造成了一個獨立的存在,這裏涉及了js 的做用域鏈和 js 回收機制,結合二者來理解就能夠了。

2、第一個例子的輸出結果分析

1. var a = fun(0); // 返回 undefined

註解:對象

  • 由於最外層的fun 函數fun(n, o)是有2個參數的,若是第二個參數沒有傳,那麼默認就會被轉換爲 undefined,因此執行以後輸出 undefined,由於 console.log 輸出的是o console.log(o);
  • 而後最外層這個 fun 函數會返回一個新對象,對象裏面有一個屬性,名爲 fun,而這個fun 屬性的值是一個匿名函數,它會返回fun(m, n);
function fun(n, o) { // ① 
  console.log(o);  // 這裏首先輸出了  n 的值爲undefined
  return { // ②  
    fun: function(m) { // ③ 
      return fun(m, n); // ④  
    }
  };
}

2. a.fun(1); // 返回 0

註解:blog

  • 因爲以前運行了var a = fun(0);,返回了一個對象,而且賦值給了變量a,因此 a 是能夠訪問對象裏面的屬性的,例如a.fun
  • a.fun(1);這裏意思是:ip

    • 訪問 a 對象的 fun 屬性,由於a 的 fun 屬性的值保存的是一個匿名函數③,因此要使用的話須要加上()
    • a.fun() 實際上調用的是 fun 屬性裏面的匿名函數,因爲匿名函數返回的fun(m, n); 沒法在當前做用域找到(由於當前做用域沒有這個定義這個函數),因此會往上找,找到了頂層的函數fun(n, o),這樣就會出現閉包的狀態,頂層的fun 函數被內層的 fun 函數引用,以前①的fun(0)的0被保存下來了,做爲 n 參數的值。
    • a.fun(1)這裏傳入了第一個參數1,因此就是 m=1,(由於③接收一個參數)。
    • 因此④的fun(m,n)就會是fun(1,0),因此輸出0
    // 已經執行過一次var a = fun(0)
    
    function fun(n, o) { // ① 
      console.log(o);
      return { // ② 
        fun: function(m) { // ③ m=1
          return fun(m, n); // ④ 不斷引用①,閉包生成,①的n 的值被保存爲0
        }
      };
    }

3. a.fun(2); // 返回 0

註解:內存

  • 這裏傳入一個參數,參數的值爲2,跟上面的a.fun(1);是同樣的流程執行。
  • 最終是 fun(2,0)執行,那麼輸出 o 就是0了
function fun(n, o) { // ① 
  console.log(o);
  return { // ② 
    fun: function(m) { // ③ 
      return fun(m, n); // ④ 
    }
  };
}

4. a.fun(3); // 返回 0

跟上面雷同,因此不作解釋了。

2、第二個例子的輸出結果分析

第二個例子實際上是一個語句,只是進行了鏈式調用,因此會有一些不同的處理。

1. 第一個返回 undefined

var b = fun(0) // 返回 undefined

註解:

  • 第一個返回 undefined 毋容置疑了,因此不說。

2. 第二個返回 0

fun(0).fun(1) // 返回 0

註解:

  • 執行fun(0)的時候返回了一個對象,對象裏面有 fun 屬性,而這個 fun 屬性的值是一個匿名函數,這個匿名函數會返回一個 fun 函數。
  • 當執行完fun(0)後,再鏈式直接執行.fun(1)的時候,它是會調用前者返回的對象裏的 fun 屬性,而且傳入了1做爲第一個參數,即m=1,而且返回的 fun 函數跟前者造成閉包,會不斷引用前者,因此 n=0 也被保存下來了。
  • 因此最終執行的時候是fun(m, n)fun(1,0),因此返回0

3. 第三個返回1

fun(0).fun(1).fun(2)

註解:

  • 執行fun(0)的時候返回了一個對象,對象裏面有 fun 屬性,而這個 fun 屬性的值是一個匿名函數,這個匿名函數會返回一個 fun 函數。
  • 當執行完fun(0)後,再鏈式直接執行.fun(1)的時候,它是會調用前者返回的對象裏的 fun 屬性,而且傳入了1做爲第一個參數,即m=1,而且返回的 fun 函數跟前者造成閉包,會不斷引用前者,因此 n=0 也被保存下來了。
  • 當再次鏈式直接執行.fun(2)的時候,這裏使用的閉包是.fun(1)返回的閉包,由於每次執行 fun 函數都會返回一個新對象,而.fun(2)引用的是.fun(1),因此 n 的值被保留爲1

    • .fun(2)返回的是fun(m, n),而這裏會跟.fun(1)(即fun(1, o))造成閉包,因此1爲 n 的值被保留。
    • 須要注意的是,js 做用域鏈只要找到可使用的,就會立刻中止向上搜索,因此.fun(2)找到.fun(1)就立刻中止向上搜索了,因此引用的是.fun(1)的值。

4. 第四個返回是2

跟第三個返回相似,因此不作解釋了。

第三個例子的輸出結果分析

// 這裏已經無需多說了,跟第二個例子相似。
var c = fun(0).fun(1); // 返回 undefined 和0

1. 第三個返回是1,第四個返回是1

c.fun(2); // 第三個返回 1
c.fun(3); // 第四個返回 1

註解:

  • 基於第一個返回和第二個返回,n 已經被賦值爲1了。
  • 而後這裏雖然屢次執行了 fun 函數,可是由於沒有再次造成閉包,n 的值沒有再次被改變,因此一直保持着1.

爲了不原文被吃掉,因此我這裏保留了截圖,而且加了一篇解釋 js 閉包還不錯的文章做爲參考使用。

相關文章
相關標籤/搜索