前端戰五渣學JavaScript——閉包

就決定是你了——閉包

有很多開發人員老是搞不清匿名函數閉包兩個概念,所以常常混用。閉包是指有權訪問另外一個函數做用域中的變量的函數。建立閉包的常見方式,就是在一個函數內部建立另外一個函數。 ——————摘自《JavaScript高級程序設計》javascript

上面說的很清楚,就是一個函數內部再建立另外一個函數(尷尬。。。好像跟書上說的同樣。多餘解釋)。我以前面試也常常被問到這個問題。。。沒想到書上就這麼一句。如下須要有一點js的基礎,瞭解做用域才能看明白。。。前端

閉包的旅程纔剛剛開始

咱們先來看一個最簡單的⬇️java

function a(){
    var b = 10;
    return function() {
        return b
    }
}
console.log(a()()); // 10
複製代碼

看,奇蹟吧!!!咱們輸出了a函數中的局部變量b的值。就是這麼神奇。。。
誒??大家可能會說那我這個局部變量b,至關於一個常量,若是我像下面⬇️這樣豈不是一個效果?面試

function a() {
    var b = 10;
    return b
}
console.log(a());
複製代碼

好的,沒有錯,那咱們再換種方式⬇️編程

function a(paramA) {
    return function b(paramB) {
        return paramA + paramB
    }
}

var variable10 = a(10);
var variable20 = a(20);

console.log(variable10(5)); // 15
console.log(variable20(5)); // 25
複製代碼

上面這個例子,在咱們執行輸出variable10(5)variable20(5)的時候,爲何咱們能獲得5與以前執行a(10)時傳進去的10呢?這就是咱們閉包的用處,能夠獲取到函數內的局部變量,並相加。
等等!也許有人不明白了,怎麼是局部變量了呢?那我按下面這種寫法可能清楚一些⬇️閉包

function a(paramA) {
  + var variableA = paramA;
  return function b(paramB) {
    return variableA + paramB
  }
}

var variable10 = a(10);
/** * a(10)是傳進去是什麼樣的呢? * * function a(10) { * var variableA = 10; * return function b(paramB) { * return variableA + paramB * } * } * 上面語法不對哦,我只是寫的好理解一些 * 也就是說variable10指向了內部函數b * var variable10 = function b(paramB) { * return 10 + paramB * } */
var variable20 = a(20);

console.log(variable10(5)); // 15
console.log(variable20(5)); // 20
複製代碼

經過上面的註釋,想必你們應該已經瞭解閉包是什麼了,就是在一個外部函數內部建立另外一個函數,以致於全局調用內部函數的時候能夠訪問到外部函數的局部變量,並使用。函數

真相永遠只有一個!

來,上《JavaScript高級程序設計》的例子ui

function createComparisonFunction(propertyName) {

  return function (object1, object2) {
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];

    if (value1 < value2) {
      return -1
    } else if (value1 > value2) {
      return 1
    } else {
      return 0;
    }
  }
}
複製代碼

在這個例子中,var value1var value2這兩行代碼訪問了外部函數中的變量propertyName,即便這個內部函數被返回了,並且是在其餘地方被調了,但它仍然能夠訪問變量propertyName;這就相似於我上面的例子(代碼接上)this

var manA = { name: '江戶川柯南', age: 7 };
var manB = { name: '工藤新一', age: 17 };
var whoOlder = createComparisonFunction('age');
// 返回-1爲前者小於後者,返回1位前者大於後者,返回0二者同樣大
console.log(whoOlder(manA, manB));  // -1,柯南小於新一
複製代碼

上面的例子就至關於開始介紹閉包時的例子,不明白?就是spa

var whoOlder = createComparisonFunction('age');
/** * var whoOlder = createComparisonFunction('age') { * var propertyName = 'age'; * return function (object1, object2) { * var value1 = object1['age']; * var value2 = object2['age']; * if (value1 < value2) { * return -1 * } else if (value1 > value2) { * return 1 * } else { * return 0; * } * } * } * 上面語法不對啊,我只是寫的好理解一些 * 最後獲得的就是 * var whoOlder = function (object1, object2) { * var value1 = object1['age']; * var value2 = object2['age']; * if (value1 < value2) { * return -1 * } else if (value1 > value2) { * return 1 * } else { * return 0; * } * } * } */
複製代碼

重點!!! 在一個函數被調用的時候,會建立一個執行環境(execution context)及相應的做用域鏈。而後,還有初始化一個活動對象(activation object),這個活動對象裏有什麼呢?有arguments和其餘命名參數的值,就是⬇️

function a(paramA, paramB) {
    return paramA + paramB
}
a(1, 2)
/** * 這個函數的活動對象有什麼呢??有三個 * arguments [1, 2] * paramA 1 * paramB 2 */
複製代碼

而後就是剛說的做用域鏈,外部函數的活動對象會始終處於第二位,就是好比我在內部函數用到變量a,會先在內部函數找有沒有聲明a這個變量,若是沒有,就會去外部函數找,外部函數沒有就會往全局執行環境找。
在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量,來看下面的例子(《JavaScript高級程序設計》例子)

function compare(value1, value2) {
    if (value1 < value2) {
        return -1
    } else if (value1 > value2) {
        return 1
    } else {
        return 0
    }
}

var result = compare(5, 10)
複製代碼

這個的做用域鏈畫出來就是

做用域鏈,執行環境,活動對象
如今應該多少能看懂這張圖了吧,當時看書的時候真是費死勁了,中間那個做用域鏈1,0什麼的,就是處於0位置的活動對象是函數本身的活動對象,處於1位置的是全局對象,因此上面說外部函數的活動對象始終處於第二位。

天下第一武道大會

想必你們面試的時候都會遇到一個很是很是經典的題,題面就是:

頁面上有十個<li></li>標籤,就是一個列表有十項,須要點擊每一個li輸出對應的索引。

經典錯誤答案:

var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}
複製代碼

這咱們如今都知道是輸出同一個數了,這是爲何呢?我來換一種寫法:

var list = document.getElementsByTagName('li');
var i = 0;
for (i; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}
複製代碼

這下咱們能夠看清楚了吧,當這段代碼都執行完,尚未點擊li的時候,全局變量i已經變成最後一個數了,假設有十個li,i就已經根據循環變成了10。因此,當咱們點擊li的輸入全局變量i的時候,固然每次輸出的都是10了。

經典正確答案:

var list = document.getElementsByTagName('li');
for (var i = 0;; i < list.length; i ++) {
  (function (i) {
    list[i].addEventListener('click', function () {
      console.log(i)
    })
  })(i)
}
複製代碼

在for循環裏,每執行一次,執行一個自調函數,並把i當作參數傳進去,js函數的參數有個按值傳遞的特性,也能夠理解爲咱們前面講過的活動對象,這個自調函數的活動對象裏面有個i的值,因此這時候每一個li上要執行的監聽函數輸出的就不在是全局變量i,而是活動對象中的i,由於循環的時候沒循環一次執行一次自調函數,因此每一個自調函數之間是各自獨立的,因此輸出的i值也天然不同了。
固然也有人會說用ES6的語法更簡單一些:

var list = document.getElementsByTagName('li');
 - for (var i = 0; i < list.length; i ++) {
 + for (let i = 0; i < list.length; i ++) {
  list[i].addEventListener('click', function () {
    console.log(i)
  })
}
複製代碼

就是簡單的把聲明i的var改爲let,這是爲何呢,這是由於ES6中新的規定let聲明的是自帶做用域的變量,而且ES6有塊級做用域的概念,因此會把每次循環的i值鎖死,天然輸出的就是不同的值啦;

有木葉的地方就會燃燒火之意志

閉包就先寫到這了,若是有哪裏寫的不對的地方但願你們指正,我立馬更改,我也不想誤人子弟。寫這篇博客也是爲了讓本身對閉包這個概念有個從新的認識,對活動對象,做用域鏈更清楚了,可是還有好多東西沒有寫,好比this指向問題,內存泄漏怎麼處理,ES5是怎麼模仿塊級做用域的,私有變量,私有函數,模塊模式,這都是一個閉包就能夠延伸出來的問題,這在《JavaScript高級程序設計》第7章第4節均可以找到,靜下心來看一看,我相信仍是能夠看明白的,一開始看不懂,就一段時間看一遍,每次確定會有不一樣的理解。

對於前端來講,追求時髦的技術當然沒錯,畢竟時髦的技術有着先進的思想,他能告訴你應該怎樣編程。可是在編程以前,仍是須要學好基礎,基礎弄明白了,確定會對js,前端,有一個全新的認識。


我是前端戰五渣,一個前端界的小學生。

相關文章
相關標籤/搜索