javascript基礎-做用域鏈及閉包

參考博客:
前端基礎進階(三):變量對象詳解
js閉包其實不難,你須要的只是瞭解什麼時候使用它前端

執行上下文

代碼在被調用時,會建立一個執行上下文,執行上下文能夠理解爲當前代碼的執行環境,它會造成一個做用域,咱們通常討論全局做用域和函數做用域兩種狀況。
執行上下文有生命週期,分別爲上下文建立階段,在該階段會肯定變量對象、做用域鏈和this指向,建立結束後會進入執行階段,完成相關賦值與調用。緩存

變量對象/活動變量

執行上下文在建立階段吧會產生變量對象,變量對象存儲了當前做用域中的變量參數和函數,當建立完成後,代碼開始執行,變量被賦予相應的值,變量對象就被激活爲活動變量。 變量對象的建立,依次經歷瞭如下幾個過程。bash

  1. 創建arguments對象。檢查當前上下文中的參數,創建該對象下的屬性與屬性值。
  2. 檢查當前上下文的函數聲明,也就是使用function關鍵字聲明的函數。在變量對象中以函數名創建一個屬性,屬性值爲指向該函數所在內存地址的引用。若是函數名的屬性已經存在,那麼該屬性將會被新的引用所覆蓋。
  3. 檢查當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名創建一個屬性,屬性值爲undefined。若是該變量名的屬性已經存在,爲了防止同名的函數被修改成undefined,則會直接跳過,原屬性值不會被修改。

第3條的含義是,首先,函數聲明的優先級要比變量高,會聲明提早到變量前面,對於函數與變量同名的狀況,同名的變量就不會被賦值undefined。固然這條只適用於建立階段階段,當代碼開始執行時,賦值就按照前後順序來。閉包

// 不一樣名狀況
console.log(a) // f a(){}
console.log(b) // undefined
function a(){}
var b=10
console.log(b) // 10
// 上述代碼的編譯執行順序爲:
(1) 函數聲明提早
function a(){}
(2) 變量聲明提早並賦值undefined
var b = undefined
(3) 編譯完成執行代碼
function a(){}
var b=undefined
console.log(a) // f a(){}
console.log(b) // undefined
b=10 // 執行時被賦值10
console.log(b) // 10


// 同名狀況
console.log(a) // f a(){}
function a(){
}
var a=10
console.log(a) // 10
// 這段代碼編譯執行的順序爲:
(1) 函數聲明提早
function a(){}
(2) 變量由於同名,因此跳過賦值undefined
(3) 編譯完成,執行代碼
function a(){}
console.log(a) // f a(){}
a=10 // 執行時被賦值10
console.log(a) // 10
複製代碼

做用域

首先要明確的是,做用域在代碼定義時就產生了,不管該段代碼在哪裏調用,做用域都不會改變。那代碼什麼時候定義?我理解的是當所處的環境被調用時,如一個函數嵌套一個函數,只有當外部函數被調用時,內部函數才被定義。
由於函數存在嵌套關係,會造成一個做用域鏈,做用域鏈是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。app

var a = 10;
var fn = null;
function outer() {    // 做用域鏈爲 本身->全局
  console.log(a)   // 10
  console.log(b)   // undefined
  var b = 20;
  console.log(b)   // 20
  function inner() {
    console.log(b)  // 20
    console.log(c)  // 報錯
  }
  fn = inner;
}
function other() {
  var c = 30;
  fn();
}
outer(); // 10 undefined 20
other(); // 20,報錯,找不到c
複製代碼

首先,是全局代碼執行,進入函數調用棧,生成全局執行上下文,分爲兩個階段,首先是建立階段,函數聲明提高,變量提高並賦值undefined,由於outer和other被定義了,因此建立了各自的做用域鏈,分別爲各自的做用域到全局做用域;建立完成後開始執行,變量被賦值,開始調用函數outer。
調用outer時,outer進入函數調用棧,開始生成outer的執行上下文,也分爲兩個階段,建立階段,變量提高,函數定義,產生變量對象放到本身的做用域中,此時inner被定義,建立了本身的做用域鏈,爲 inner的做用域到outer的做用域再到全局做用域;outer的執行上下文建立完成後開始執行相應代碼,依次輸出10 unndefined 20;而後執行結束,彈出函數調用棧。
接着調用other,也是一樣的步驟,注意執行到fn時,由於fn被賦值inner,做用域跟inner同樣,而inner的做用域鏈在定義的時候就肯定了,不管在哪裏執行都不會改變,因此此時inner的做用域鏈也只有本身、outer和全局的,並不能訪問到other中的變量,因此找不到c。函數

閉包

其實上述代碼就是閉包的場景,outer雖然調用結束了,可是由於做用域鏈的緣由,並不會被摧毀回收,inner仍然能夠訪問到做用域鏈上的變量。
簡單來講,閉包就是外部函數能夠訪問某個函數內部變量的機制,因此造成閉包的必要條件就是,函數嵌套函數,同時將內部函數返回出來。性能

爲何要有閉包

  • 延長變量的生命週期同時不會污染全局變量
  • 能夠緩存數據

(1)點贊功能,每一個點贊按鈕點擊加1,同時互不影響ui

function add(){
    var count=0;
    return function(){
        return count++
    }
}
// 給按鈕綁定不一樣的點擊事件
for(i in n){
    var fn=add()
    btn[i].onclick=fn;
}

複製代碼

(2)數據緩存.假若有一個計算乘積的函數,mult函數接收一些number類型的參數,並返回乘積結果。爲了提升函數性能,咱們增長緩存機制,將以前計算過的結果緩存起來,下次遇到一樣的參數,就能夠直接返回結果,而不須要參與運算。這裏,存放緩存結果的變量不須要暴露給外界,而且須要在函數運行結束後,仍然保存,因此能夠採用閉包。this

var mult = (function(){
    var cache = {};
    var calculate = function() {
        var a = 1;
        for(var i = 0, len = arguments.length; i < len; i++) {
            a = a * arguments[i];
        }
        return a;
    }
    
    return function() {
        var args = Array.prototype.join.call(arguments, ',');
        if(args in cache) {
            return cache[args];
        }
        
        return cache[args] = calculate.apply(null, arguments);
    }
}())
複製代碼

感受閉包仍是須要多寫案例才能真正弄得清楚,爲何這麼設計,會持續更新。spa

相關文章
相關標籤/搜索