【進階2-3期】JavaScript深刻之閉包面試題解

本期的主題是做用域閉包,本計劃一共28期,每期重點攻克一個面試重難點,若是你還不瞭解本進階計劃,文末點擊查看所有文章。javascript

若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。html


做用域指的是一個變量和函數的做用範圍,JS中函數內聲明的全部變量在函數體內始終是可見的,在ES6前有全局做用域和局部做用域,可是沒有塊級做用域(catch只在其內部生效),局部變量的優先級高於全局變量。前端

做用域

變量提高
var scope="global";
function scopeTest(){
    console.log(scope);
    var scope="local"  
}
scopeTest(); //undefined
複製代碼

上面的代碼輸出是undefined,這是由於局部變量scope變量提高了,等效於下面java

var scope="global";
function scopeTest(){
    var scope;
    console.log(scope);
    scope="local"  
}
scopeTest(); //undefined
複製代碼

注意,若是在局部做用域中忘記var,那麼變量就被聲明爲全局變量。webpack

沒有塊級做用域
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();	// 3
data[1]();	// 3
data[2]();	// 3
複製代碼

上篇文章已經介紹過了,【進階2-2期】JavaScript深刻之從做用域鏈理解閉包git

做用域鏈

每一個函數都有本身的執行上下文環境,當代碼在這個環境中執行時,會建立變量對象的做用域鏈,做用域鏈是一個對象列表或對象鏈,它保證了變量對象的有序訪問。github

做用域鏈的開始是當前代碼執行環境的變量對象,常被稱之爲「活躍對象」(AO),變量的查找會從第一個鏈的對象開始,若是對象中包含變量屬性,那麼就中止查找,若是沒有就會繼續向上級做用域鏈查找,直到找到全局對象中web

閉包

function createClosure(){
    var name = "jack";
    return {
        setStr:function(){
            name = "rose";
        },
        getStr:function(){
            return name + ":hello";
        }
    }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello
複製代碼

上面在函數中返回了兩個閉包,這兩個閉包都維持着對外部做用域的引用。閉包中會將外部函數的自由對象添加到本身的做用域鏈中,因此能夠經過內部函數訪問外部函數的屬性,這也是javascript模擬私有變量的一種方式。面試

閉包面試題解

因爲做用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引發的一個反作用就是若是內部函數在一個循環中,那麼變量的值始終爲最後一個值。算法

這個代碼已經貼過了,怕大家忘記,就再貼一遍

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();	// 3
data[1]();	// 3
data[2]();	// 3
複製代碼

若是要強制返回預期的結果,怎麼辦???

方法1:當即執行函數
for (var i = 0; i < 3; i++) {
    (function(num) {
        setTimeout(function() {
            console.log(num);
        }, 1000);
    })(i);
}
// 0
// 1
// 2
複製代碼
方法2:返回一個匿名函數賦值
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (num) {
      return function(){
          console.log(num);
      }
  })(i);
}

data[0]();	// 0
data[1]();	// 1
data[2]();	// 2
複製代碼

不管是當即執行函數仍是返回一個匿名函數賦值,原理上都是由於變量的按值傳遞,因此會將變量i的值複製給實參num,在匿名函數的內部又建立了一個用於訪問num的匿名函數,這樣每一個函數都有了一個num的副本,互不影響了。

方法3:使用ES6中的let
var data = [];

for (let i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();
複製代碼

解釋下原理

var data = [];// 建立一個數組data;

// 進入第一次循環
{ 
	let i = 0; // 注意:由於使用let使得for循環爲塊級做用域
	           // 這次 let i = 0 在這個塊級做用域中,而不是在全局環境中
    data[0] = function() {
    	console.log(i);
	};
}
複製代碼

循環時,let聲明i,因此整個塊是塊級做用域,那麼data[0]這個函數就成了一個閉包。這裏用{}表達並不符合語法,只是但願經過它來講明let存在時,這個for循環塊是塊級做用域,而不是全局做用域。

上面的塊級做用域,就像函數做用域同樣,函數執行完畢,其中的變量會被銷燬,可是由於這個代碼塊中存在一個閉包,閉包的做用域鏈中引用着塊級做用域,因此在閉包被調用以前,這個塊級做用域內部的變量不會被銷燬。

// 進入第二次循環
{ 
	let i = 1; // 由於 let i = 1 和上面的 let i = 0 
	           // 在不一樣的做用域中,因此不會相互影響
	data[1] = function(){
         console.log(i);
	}; 
}
複製代碼

當執行data[1]()時,進入下面的執行環境。

{ 
     let i = 1; 
     data[1] = function(){
          console.log(i);
     }; 
}
複製代碼

在上面這個執行環境中,它會首先尋找該執行環境中是否存在i,沒有找到,就沿着做用域鏈繼續向上到了其所在的塊做用域執行環境,找到了i = 1,因而輸出了1

思考題

代碼1:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

var foo = checkscope(); 
foo();                    
複製代碼

代碼2:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}

checkscope();  
複製代碼

上面的兩個代碼中,checkscope()執行完成後,閉包f所引用的自由變量scope會被垃圾回收嗎?爲何?

參考

深刻javascript——做用域和閉包

ES6之let(理解閉包)和const命令

進階系列目錄

  • 【進階1期】 調用堆棧
  • 【進階2期】 做用域閉包
  • 【進階3期】 this全面解析
  • 【進階4期】 深淺拷貝原理
  • 【進階5期】 原型Prototype
  • 【進階6期】 高階函數
  • 【進階7期】 事件機制
  • 【進階8期】 Event Loop原理
  • 【進階9期】 Promise原理
  • 【進階10期】Async/Await原理
  • 【進階11期】防抖/節流原理
  • 【進階12期】模塊化詳解
  • 【進階13期】ES6重難點
  • 【進階14期】計算機網絡概述
  • 【進階15期】瀏覽器渲染原理
  • 【進階16期】webpack配置
  • 【進階17期】webpack原理
  • 【進階18期】前端監控
  • 【進階19期】跨域和安全
  • 【進階20期】性能優化
  • 【進階21期】VirtualDom原理
  • 【進階22期】Diff算法
  • 【進階23期】MVVM雙向綁定
  • 【進階24期】Vuex原理
  • 【進階25期】Redux原理
  • 【進階26期】路由原理
  • 【進階27期】VueRouter源碼解析
  • 【進階28期】ReactRouter源碼解析

交流

進階系列文章彙總:github.com/yygmind/blo…,內有優質前端資料,歡迎領取,以爲不錯點個star。

我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!

相關文章
相關標籤/搜索