天天看點

js 【詳解】閉包

閉包的概念

如果一個函數通路了此函數的父級及父級以上的作用域變量,那麼這個函數就是一個閉包。

是以以下寫法都是閉包

var a = 1;
    // 匿名的立即執行函數,因通路了全局變量a,是以也是一個閉包
    (function test (){
        alert(a);
    })()      
本質上,JS中的每個函數都是一個閉包,因為每個函數都可以通路全局變量。

關于JS中的作用域,可以參考部落格:

實作閉包最常見的方式就是函數嵌套(并不是形成閉包的唯一方式!)

function a() {
    var i = '初始值';
    i = i + "—_執行a"
    // 此處的函數b通路了父級函數a中的局部變量i,成為了一個閉包
    function b() {
        i = i + "_執行b"
        console.log(i)
    }
    return b;
}
var c = a(); // 此時 i 的值為 :初始值—_執行a
c()          // 此時 i 的值為 :初始值—_執行a_執行b
c()          // 此時 i 的值為 :初始值—_執行a_執行b_執行b      

閉包的執行過程

以上方代碼為例:

  1. 将函數a指派給全局變量c時,a會執行一次,局部變量 i 的值變為​

    ​初始值—_執行a​

    ​,最終傳回函數b,此時全局變量c的值為閉包函數b的引用。

此時函數a雖然已執行完,但因為内部包含閉包函數b,是以函數 a 的執行期上下文會繼續保留在記憶體中,不會被銷毀,是以局部變量 i 仍是​

​初始值—_執行a​

執行期上下文:當函數執行時,會建立一個執行期上下文的内部對象。每調用一次函數,就會建立一個新的上下文對象,他們之間是互相獨立的。當函數執行完畢,它所産生的執行期上下文會被銷毀
  1. 第一次執行​

    ​c()​

    ​​ 時,閉包函數b第一次執行,局部變量 i 的值變為​

    ​初始值—_執行a_執行b​

  2. 第二次執行​

    ​c()​

    ​​ 時,閉包函數b第二次執行,局部變量 i 的值變為​

    ​初始值—_執行a_執行b_執行b​

閉包的圖解

var a = "global variable";
var F = function () {
  var b = "local variable";
  var N = function () {
    var c = "inner local";
    return b;
  };
 return N;
};
var d = F()
d()      

G 為全局作用域

js 【詳解】閉包
  • 全局作用域 G 中有:

    —— 函數 F

    —— 全局變量 a

    —— 全局變量 d (存有對閉包函數 N 的引用)

  • 函數 F 中有: (傳回閉包函數N)

    —— 函數 F 作用域中的局部變量 b

    —— 閉包函數 N

  • 閉包函數 N 中有: (傳回局部變量b)

    —— 函數 N 作用域中的局部變量 c

閉包的特點

1.被閉包函數通路的父級及以上的函數的局部變量(如範例中的局部變量 i )會一直存在于記憶體中,不會被JS的垃圾回收機制回收。

2.閉包函數實作了對其他函數内部變量的通路。(函數内部的變量對外是無法通路的,閉包通過這種變通的方法,實作了通路。)

Javascript的垃圾回收機制
  • 如果一個對象不再被引用,那麼這個對象就會被GC回收。
  • 如果兩個對象互相引用,而不再被第三者所引用,那麼這兩個對象都會被回收。

閉包的用途

  1. 通路函數内部的變量
  2. 讓變量始終保持在記憶體中

閉包的應用場景

模拟面向對象的代碼風格

比如模拟兩人對話

function person(name) {
    function say(content) {
        console.log(name + ':' + content)
    }
    return say
}

a = person("張三")
b = person("李四")
a("在幹啥?")
b("沒幹啥。")
a("出去玩嗎?")
b("去哪啊?")      

控制台列印結果為:

張三:在幹啥?
李四:沒幹啥。
張三:出去玩嗎?
李四:去哪啊?      

使setTimeout支援傳參

通過閉包實作setTimeout第一個函數傳參(預設不支援傳參)

function func(param){
    return function(){
        alert(param)
    }
}
var f1 = func(1);
setTimeout(f1,1000);      

封裝私有變量

//用閉包定義能通路私有函數和私有變量的公有函數。
var counter = (function () {
    var privateCounter = 0; //私有變量
    function change(val) {
        privateCounter += val;
    }
    return {
        increment: function () {
            change(1);
        },
        decrement: function () {
            change(-1);
        },
        value: function () {
            return privateCounter;
        }
    };
})();

console.log(counter.value());//0
counter.increment();
console.log(counter.value());//1
counter.increment();
console.log(counter.value());//2      

模拟塊作用域

js 【詳解】閉包

依次點選 4 個 li 标簽,結果都彈出 4

解析:onclick綁定的function中沒有變量 i,解析引擎會尋找父級作用域,最終找到了全局變量 i,for循環結束時,i 的值已變成了4,是以onclick事件執行時,全都彈出 4。

下面使用閉包來解決這個問題:

var elements = document.getElementsByTagName('li');
var length = elements.length;
for (var i = 0; i < length; i++) {
    elements[i].onclick = function (num) {
        return function () {
            alert(num);
        };
    }(i);
}      

通過匿名閉包,把每次的 i 都儲存到一個變量中,實作了預期效果。

當然,通過 ES6 的 let 可以輕松解決這個問題:

var elements = document.getElementsByTagName('li');
var length = elements.length;
for (let i = 0; i < length; i++) {
    elements[i].onclick = function () {
        alert(i);
    };
}      

實作疊代器

function setup(x) {
  var i = 0;
  return function(){
    return x[i++];
  };
}
var next = setup(['a', 'b', 'c']);      

控制台中的執行效果:

> next();
"a"
> next();
"b"
> next();
"c"      

閉包的優點

  1. 可以減少全局變量的定義,避免全局變量的污染
  2. 能夠讀取函數内部的變量
  3. 在記憶體中維護一個變量,可以用做緩存

閉包的缺點

1)造成記憶體洩露

閉包會使函數中的變量一直儲存在記憶體中,記憶體消耗很大,是以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導緻記憶體洩露。

解決方法——使用完變量後,手動将它指派為null;

2)閉包可能在父函數外部,改變父函數内部變量的值。

3)造成性能損失

由于閉包涉及跨作用域的通路,是以會導緻性能損失。

解決方法——通過把跨作用域變量存儲在局部變量中,然後直接通路局部變量,來減輕對執行速度的影響

閉包的範例

傳回匿名閉包

function funA(){
  var a = 10;  // funA的活動對象之中;
  return function(){   //匿名函數的活動對象;
        alert(a);
  }
}
var b = funA();
b();  //10      

各自獨立的閉包

function outerFn(){
  var i = 0; 
  function innerFn(){
      i++;
      console.log(i);
  }
  return innerFn;
}
var inner = outerFn();  //每次外部函數執行的時候,都會開辟一塊記憶體空間,外部函數的位址不同,都會重新建立一個新的位址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2();   //1 2 3 1 2 3      
function fn(){
  var a = 3;
  return function(){
    return  ++a;                                     
  }
}
alert(fn()());  //4
alert(fn()());  //4      

通路全局變量的閉包

var i = 0;
function outerFn(){
  function innnerFn(){
       i++;
       console.log(i);
  }
  return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2();     //1 2 3 4      

寫法有點繞的閉包

(function() { 
  var m = 0; 
  function getM() { return m; } 
  function seta(val) { m = val; } 
  window.g = getM; 
  window.f = seta; 
})(); 
f(100);
console.info(g());   //100  閉包找到的是同一位址中父級函數中對應變量最終的值      

閉包的鍊式調用

var add = function (x) {
    var sum = 1;
    var tmp = function (x) {
        console.log('執行tmp')
        sum = sum + x;
        return tmp;
    }
    tmp.toString = function () {
        return sum;
    }
    return tmp;
}
console.log(add(1)(2)(3).toString())      
執行tmp
執行tmp
6      
console.log(add(8)(2)(3).toString()) // 最終結果還是 6      

留意父函數執行過一次!

function love1(){
     var num = 223;
     var me1 = function() {
           console.log(num);
     }
     num++;
     return me1;
}
var loveme1 = love1();
loveme1();   //輸出224      

列印每次鍊式調用的上一次傳參

function fun(n,o) {
    console.log(o);
    return {
         fun:function(m) {
               return fun(m,n);
         }
    };
}
var a = fun(0);  //undefined
a.fun(1);  //0  
a.fun(2);  //0  
a.fun(3);  //0  
var b = fun(0).fun(1).fun(2).fun(3);   //undefined  0  1  2
var c = fun(0).fun(1);  
c.fun(2);  
c.fun(3);  //undefined  0  1  1      

繼續閱讀