由 for 循環經典面試題延伸的 js 相關知識

以前工做中碰到一個需求,須要根據從後臺獲取到的圖片路徑得到這些圖片的 base64 文件。實現過程當中遇到一個問題,代碼以下:面試

var src=["http://www.w3school.com.cn/i/site_photoref.jpg",
    "http://www.w3school.com.cn/i/site_photoexa.jpg",
    "http://www.w3school.com.cn/i/site_photoqe.jpg"] 
for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(img)   
        //最終打印出來都是最後一個圖片
        //<img src="http://www.w3school.com.cn/i/site_photoqe.jpg">
        //<img src="http://www.w3school.com.cn/i/site_photoqe.jpg">
        //<img src="http://www.w3school.com.cn/i/site_photoqe.jpg">
    }
}

這個問題其實跟以前常常碰到的一個面試題本質上是一致的。咱們能夠在上面函數中打印索引值,會發現打印出來的值都是3。通過 segmentfault 上網友的點撥,這個問題涉及到 js 中的兩個問題,做用域鏈事件執行機制ajax

做用域鏈

在 js 中,每一個函數都有本身的執行環境,每一個執行環境都有一個與之關聯的變量對象。當代碼在一個環境中執行時,會建立變量對象的一個做用域鏈。每一個做用域鏈的起點都是當前執行代碼所在的執行環境的變量對象,做用域鏈的下一個變量對象來自包含環境,一直延續到全局執行環境(瀏覽器中是指 window 對象)。
若是執行環境是函數,那麼它的變量對象就包括活動對象,活動對象在一開始只包括 arguments 對象(函數的參數對象)。segmentfault

當執行環境中要用到某個變量或者函數時,會從本身做用域鏈的起點也就是本身的變量對象中開始搜索相應的變量名或者函數名,若是搜索不到就接着在做用域鏈的上一級搜索,一直到找到相關變量名或者到做用域鏈的末尾爲止。瀏覽器

js 事件執行機制

js 是一種單線程語言,在主線程中同一時間只能執行一個任務。多線程

瀏覽器內核線程

瀏覽器內核是多線程的,一般包含如下線程:異步

  • GUI 渲染線程:
    負責渲染網頁,當頁面須要重繪時,該線程就會執行。函數

  • JavaScript 引擎線程:
    也就是 JS 內核,負責解析和運行 JS 代碼。oop

  • 定時器觸發器線程:
    經過這個線程計時來肯定何時觸發定時器。this

  • 事件觸發線程:
    監控某個事件是否觸發,事件觸發以後會被添加到任務隊列中。spa

  • 異步 HTTP 請求線程:
    監控 AJAX 的狀態變動時,就會把相應的任務添加到任務隊列中。

同步和異步

js 中每一個任務的操做能夠簡化爲發起調用得到結果兩步,根據這兩步能夠把js 中的任務能夠分爲同步任務和異步任務。全部任務的執行都在主線程進行。
同步任務:發起調用以後,當即就會執行來獲取結果的任務。調用以後會一直等待直到返回結果,在這期間主線程不能進行其餘操做。
異步任務:發起調用以後,並不會當即執行相關函數,而是須要額外的操做知足相關條件以後進行觸發。相關任務被觸發以後會進入任務隊列等待主線程任務執行完成後按順序進入主線程,調用和執行之間的時間能夠介入其餘異步任務。常見的異步任務有定時器、ajax和事件回調等。

事件循環機制(event loop)

js 中事件執行基本按照下面這三步進行循環。

  • 主線程先按照代碼順序執行同步任務

  • 在異步任務被註冊以後,瀏覽器的其餘線程(事件觸發線程、定時器觸發線程、異步 HTTP 請求線程)監控異步任務的觸發條件,按照觸發順序把這些異步任務放在任務隊列中

  • 主線程上同步任務執行完以後,會依次執行任務隊列中的任務

回到開頭

在最上面的例子中,for 循環是同步任務,會當即執行,圖片的 onload 事件是異步任務,須要等另行觸發。這個例子中代碼的執行順序是這樣:

同步任務:循環建立三張圖片,每一個圖片賦予各自的 src 值,而且都註冊了一個 onload 事件。
異步任務:三張圖片的 onload 事件依次觸發,回調函數進入任務隊列,等主線程的 for 循環執行完畢以後,依次執行這三個任務。

當開始執行異步任務時,每一個函數都須要用到 img 這個變量,就開始在本身的做用域鏈上開始尋找 img,自身變量對象中不存在,接着在包含環境中找到,因爲 for 循環並不會創造一個新的執行環境,因此這個例子中包含環境其實就是全局執行環境。而在 for 循環完以後,img 變量的值已經通過兩次覆蓋變成了最後一個索引對應的圖片。因此每一個圖片的 onload 函數都會打印出同一個 img 。打印 i 值出現的結果也是同樣。
event-loop.png

解決辦法

弄清楚出現這個問題的緣由,解決這個問題能夠用下面的辦法:

方法1:建立單獨的執行環境

for(var i=0;i<3;i++){
    (function(index){   
        var img=new Image();
        img.src=src[i];
        img.onload=function(){
            console.log(index)
            console.log(img)   
        }
    })(i)
}

這個方法實現的原理是:for 循環中當即執行函數每次都會建立一個新的執行環境,三張圖片的 onload 事件函數的做用域鏈的包含環境分別是這三個當即執行函數,這三個當即執行函數裏面保存的是不一樣的 img 變量和不一樣的參數 index,當三個 onload 回調函數執行時,分別在本身的做用域鏈上尋找各自對應的 img 變量。利用一樣的原理,也能夠寫成這樣:

for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(index,img){
        return function(){
            console.log(index)
            console.log(img) 
        }  
    }(i,img)
}

也能夠不經過傳參,而是在當即執行函數內部建立一個變量來接收每次循環中的 i 值,原理都是同樣的。

方法2:訪問事件觸發節點

for(var i=0;i<3;i++){
    var img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(this)   
    }
}

這個方法的原理是:函數內部在執行過程當中會有一個默認的 this 變量會把函數的調用對象保存起來,經過函數內部的 this 就能夠訪問調用函數的對象。或者能夠經過 event 事件對象的 currentTarget 屬性訪問到事件觸發節點,原理是同樣的。

方法3:ES6 的新語法 let

for(let i=0;i<3;i++){
    let img=new Image();
    img.src=src[i];
    img.onload=function(){
        console.log(img)   
        console.log(i)
    }
}

在 ES6 中規定了一個新的變量聲明命令 let,let 會建立一個塊級做用域,用 let 聲明的變量只在 let 所在的代碼塊中有效。這個例子中,三次循環會建立三個塊級做用域,每一個塊級做用域中有各自的變量 i 和 img,互相獨立,每一個 onload 回調函數執行時都會獲取各自代碼塊中的 i 和 img ,最終能實現咱們想要的結果。

相關文章
相關標籤/搜索