1. 程式人生 > 實用技巧 >Js閉包使用姿勢指南

Js閉包使用姿勢指南

引言

閉包就是指能夠訪問另一個函式作用域的變數的函式,閉包就是一個函式,能夠訪問其他函式的作用域中的變數,js有一個全域性物件,在瀏覽器下是window,node下是global,所有的函式都在這個物件下,也能訪問這個物件下的變數,這也就是說,js中的所有函式都是閉包

閉包的定義

函式與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函式訪問外部函式作用域。在JavaScript,函式在每次建立時生成閉包。

MDN對閉包的定義中說道了詞法環境和引用同時也說道了每次建立時生成閉包

參考程式碼

const eg = ()=>{
    let a ='測試變數' // 被eg建立的區域性變數
    let inner = ()=>{ // eg的內部函式,一個閉包
        console.log(a) // 使用了父函式中宣告的變數
    }
    return inner // inner就是一個閉包函式 可以訪問到eg函式的作用域
}

來個有趣的例子吧

function init() {
    var name = "Mozilla"; // name 是一個被 init 建立的區域性變數
    function displayName() { // displayName() 是內部函式,一個閉包
        alert(name); // 使用了父函式中宣告的變數
    }
   displayName();
 }
 init();

由於js作用域的原因,dispplayName可以訪問到父級作用域init的變數name,這點母庸質疑

那麼再看這個例子

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
  }
var myFunc = makeFunc();
myFunc();

這段程式碼和之前的程式碼執行結果完全一樣,其中的不同 — 也是有意思的地方 — 在於內部函式displayName()在執行前,被外部函式返回。你很可能認為它無法執行,那麼我們再改變一下程式碼

var name2 = 123
function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name2);
    }
    return displayName;
  }
var myFunc = makeFunc();
myFunc();

你幾乎不用想就能知道結果肯定是123那麼我們在返回之前的程式碼,為什麼你就無法肯定程式碼的執行結果了呢

答案是,JavaScript中的函式會形成閉包。 閉包是由函式以及建立該函式的詞法環境組合而成。請仔細閱讀這段話,js的閉包是由函式及建立該函式的詞法環境組合而成,建立它的詞法環境有這個變數,所有直接使用這個變數,沒有則向上查詢,直至在全域性環境都找不到,返回undefind

那麼我們再把例子換一下

var object = {
     name: ''object",
     getName: function() {
        return function() {
             console.info(this.name)
        }
    }
}
object.getName()()    // underfined

這個時候this指向哪裡呢?答案是全域性因為裡面的閉包函式是在window作用域下執行的,也就是說,this指向windows

現在我們換個例子吧

function outer() {
     var  a = '變數1'
     var  inner = function () {
            console.info(a)
     }
     return inner    // inner 就是一個閉包函式,因為他能夠訪問到outer函式的作用域
}
var  inner = outer()   // 獲得inner閉包函式
inner()   //"變數1"

當程式執行完var inner = outer(),其實outer的執行環境並沒有被銷燬,因為他裡面的變數a仍然被被inner的函式作用域鏈所引用,當程式執行完inner(), 這時候,inner和outer的執行環境才會被銷燬調;《JavaScript高階程式設計》書中建議:由於閉包會攜帶包含它的函式的作用域,因為會比其他函式佔用更多內容,過度使用閉包,會導致記憶體佔用過多。

我們再來個有趣的例子

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
 }

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

add5和add10都是閉包,也共享函式的定義,但是儲存了不同的詞法環境,在add5中x=5而在add10中x為10

記憶體洩露問題

閉包函式引用外層的變數,當執行完外層函式是,變數會無法釋放

function  showId() {
    var el = document.getElementById("app")
    el.onclick = function(){
      aler(el.id)   // 這樣會導致閉包引用外層的el,當執行完showId後,el無法釋放
    }
}

// 改成下面function  showId() {
    var el = document.getElementById("app")
    var id  = el.id
    el.onclick = function(){
      aler(id)   // 這樣會導致閉包引用外層的el,當執行完showId後,el無法釋放
    }
    el = null    // 主動釋放el
}
function  factorial(num) {
   if(num<= 1) {
       return 1;
   } else {
      return num * factorial(num-1)
   }}var anotherFactorial = factorial
factorial = nullanotherFactorial(4)   // 報錯 ,因為最好是return num* arguments.callee(num-1),arguments.callee指向當前執行函式,但是在嚴格模式下不能使用該屬性也會報錯,所以藉助閉包來實現


// 使用閉包實現遞迴function newFactorial = (function f(num){
    if(num<1) {return 1}
    else {
       return num* f(num-1)
    }
}) //這樣就沒有問題了,實際上起作用的是閉包函式f,而不是外面的函式newFactorial

用閉包模擬私有方法

程式語言中,比如 Java,是支援將方法宣告為私有的,即它們只能被同一個類中的其它方法所呼叫。這個方式也稱為 模組模式(module pattern)

而 JavaScript 沒有這種原生支援,但我們可以使用閉包來模擬私有方法。私有方法不僅僅有利於限制對程式碼的訪問:還提供了管理全域性名稱空間的強大能力,避免非核心的方法弄亂了程式碼的公共介面部分。

下面的示例展現瞭如何使用閉包來定義公共函式,並令其可以訪問私有函式和變數。

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

在之前的示例中,每個閉包都有它自己的詞法環境;而這次我們只建立了一個詞法環境,為三個函式所共享:Counter.increment,Counter.decrement和Counter.value。

該共享環境創建於一個立即執行的匿名函式體內。這個環境中包含兩個私有項:名為privateCounter的變數和名為changeBy的函式。這兩項都無法在這個匿名函式外部直接訪問。必須通過匿名函式返回的三個公共函式訪問。

這三個公共函式是共享同一個環境的閉包。多虧 JavaScript 的詞法作用域,它們都可以訪問privateCounter變數和changeBy函式。

你應該注意到我們定義了一個匿名函式,用於建立一個計數器。我們立即執行了這個匿名函式,並將他的值賦給了變數Counter。我們可以把這個函式儲存在另外一個變數makeCounter中,並用他來建立多個計數器。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

請注意兩個計數器Counter1和Counter2是如何維護它們各自的獨立性的。每個閉包都是引用自己詞法作用域內的變數privateCounter。

每次呼叫其中一個計數器時,通過改變這個變數的值,會改變這個閉包的詞法環境。然而在一個閉包內對變數的修改,不會影響到另外一個閉包中的變數。

以這種方式使用閉包,提供了許多與面向物件程式設計相關的好處 —— 特別是資料隱藏和封裝。

在迴圈中使用閉包

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerhtml = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

看到這裡你一定能想到,由於共享了同一個詞法作用域,最終結果是所有的item.help都指向了helptext的最後一項,解決方法是使用let關鍵字或者使用匿名閉包

// 匿名閉包
function showHelp(help) {
  document.getElementById('help').innerhtml = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    (function() {
       var item = helpText[i];
       document.getElementById(item.id).onfocus = function() {
         showHelp(item.help);
       }
    })(); // 馬上把當前迴圈項的item與事件回撥相關聯起來
  }
}
setupHelp();

// 使用let關鍵字
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    let item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

效能考慮

如果不是某些特定任務需要使用閉包,在其它函式中建立函式是不明智的,因為閉包在處理速度和記憶體消耗方面對指令碼效能具有負面影響。

例如,在建立新的物件或者類時,方法通常應該關聯於物件的原型,而不是定義到物件的構造器中。原因是這將導致每次構造器被呼叫時,方法都會被重新賦值一次(也就是,每個物件的建立)。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

在上面的程式碼中,我們並沒有利用到閉包的好處,因此可以避免使用閉包。修改成如下:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();}MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

也可以這樣

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;};MyObject.prototype.getMessage = function() {
  return this.message;
};