經典題目談閉包

侃侃閉包

春暖花開,又到了程序猿換領地的季節了,各大論壇出現不少的面試題和各類押題,而後我和小夥伴仔細研究,果真大部分不會。其中有一個講zepto源碼的,提到了js三座大山:閉包原型異步。我曾經入門的時候在這3個山裏面饒了好久好久,並且屢次覺得繞出去的時候才發現我只是過了一個小山頭,苦不堪言。固然,我以爲如今本身已經繞出來了,那麼就先講一講閉包這座山的路。javascript

經典回顧

先放一段經典的面試題java

for(var i = 0; i < 10; i ++) {
    bt[i].onclick = function(){
        console.log(i);
    }
}

初次接觸閉包就是這個問題,可是這樣看,好像和閉包沒有什麼關係,並且確實沒有什麼關係。這個每次結果都是9已經毫無疑問了,可是爲何是9?咱們慢慢拆解。面試

var i;
for(i = 0; i < 10; i ++) {
    bt[i].onclick = function(){
        console.log(i);
    }
}

i做爲變量被提高了,咱們再把循環展開瀏覽器

bt[0].onclick = function(){
    console.log(i);
}
bt[1].onclick = function(){
    console.log(i);
}
bt[2].onclick = function(){
    console.log(i);
}
....

這時有人確定和我當年有同樣的疑問,爲何function裏的是i,不該該對應的是當時的i的值嗎?閉包

先舉個例子異步

var i = 10;
function test(){
    var i = 1;
    console.log(i);
}

若是i在js加載就賦值了,那麼是否是就成了console.log(10),test()執行後打印的應該是10而不是1啊。很明顯,解析js的時候,test這個函數內部並無賦值,只是給它了一個變量,具體什麼值等調用了纔會拿到。其實拆到這的時候我也很懵,什麼鬼,爲何會是這樣。函數

執行上下文

在ES6以前是沒有塊級做用域這個概念的,只有全局做用域、函數做用域和eval做用域。this

在瀏覽器加載js腳本的時候,解析器在解析js代碼時,會先進入全局做用域,若是碰到函數調用,會創造函數上下文,並將這個上下文壓入到執行棧中,若是這個函數內部還有另外一個函數調用,那麼就會把另外一個函數壓入到執行棧的頂部,依次類推,最後調用的函數老是被放到執行棧的最頂部,而後執行最上層的函數。線程

這個過程涉及到js很重要的幾個點:js是單線程同步執行函數被調用的時候纔會建立執行上下文code

既然知道了函數調用的時候會建立執行上下文,那麼就要探究下怎麼執行上下文內部怎麼執行的:

  • 初始化做用域鏈
  • 建立變量對象:建立 參數對象, 檢查參數的上下文, 初始化其名稱和值並建立一個引用拷貝
  • 掃描上下文中的函數聲明:對於每一個被發現的函數, 在 變量對象 中建立一個和函數名同名的屬性,這是函數在內存中的引用
  • 掃描上下文中的變量聲明:1. 對於每一個被發現的變量聲明,在變量對象中建立一個同名屬性並初始化值爲 undefined。2. 若是變量名在 變量對象 中已經存在, 什麼都不作,繼續掃描。
  • 肯定上下文中的 "this"
  • 激活 / 代碼執行階段:執行 / 在上下文中解釋函數代碼,並在代碼逐行執行時給變量賦值。

上面這些步驟能夠分爲2個階段:初始化階段(建立階段)和執行階段。

咱們看個例子

var a = 100;
function fn(n) {
    var a = 10;
    const b = function(){};
    function c(o) {}
}

當在調用fn(11)時,建立階段實際上是這樣的:

function fn(n) {
    a:100  //父級上下文中的變量(做用域鏈)
    arguments:{
        0:11,
        length:1
    }
    c:function c(o)
    a: undefined
    b: undefined
    this:{//全局是window}
}

很明顯,在建立階段只是作了屬性名的定義,並無給函數內變量賦值,全局變量和參數除外。建立階段完成後,便開始進入執行階段。代碼執行階段看起來是這樣得:

function fn(n) {
    a:100  //父級上下文中的變量(做用域鏈)
    arguments:{
        0:11,
        length:1
    }
    c:function c(o)
    a: 10
    b: function()
    this:{//全局是window}
}

這就是整個執行環境。

閉包

說了這麼多廢(pu)話(dian),終於到正題了。紅皮書是這樣說的:閉包是有權訪問另外一個函數做用域中的變量的函數。若是熟讀紅皮書的話應該很好理解這句話,主要在做用域鏈那。固然通俗點就是內部能夠訪問外部,外部不能訪問內部。

如今返回去看看那道經典的題目,應該很好解決了。根據執行上下文的過程,咱們在外層加一個函數,而且執行它,由於初始化過程當中參數是能夠直接被賦值的。

for(var i = 0; i < 10; i ++) {
    (function(num){
        bt[i].onclick = function(){
            console.log(num);
        }
    })(i)
    
}

這樣就ok了。

總結

閉包並不可怕,只要搞清js的一些執行機制,不少也就迎刃而解了。當時我並不理解爲何要加一個理解執行函數,後來看到不少關於做用域鏈和執行上下文的文章,反過來想一想,也就沒什麼難的了。固然,閉包一個很重要的做用就是設置私有變量,其實這個題目也算一個。

這個講的很簡單,若是仔細說閉包,估計要說2小時。這個只是來理解這一個題目而已。

他的原創之處並不優秀,他的優秀之處並不是原創!!!

相關文章
相關標籤/搜索