閉包的使用場景,使用閉包須要注意什麼

閉包

什麼是閉包

閉包很簡單,就是可以訪問另外一個函數做用域變量的函數,更簡單的說,閉包就是函數,只不過是聲明在其它函數內部而已。css

例如:html

function getOuter(){
  var count = 0
  function getCount(num){
    count += num
    console.log(count) //訪問外部的date
  }
  return getCount //外部函數返回
}
var myfunc = getOuter()
myfunc(1) // 1
myfunc(2) // 3

myfunc 就是閉包, myfunc 是執行 getOuter 時建立的 getCount 函數實例的引用。 getCount 函數實例維護了一個對它的詞法環境的引用,因此閉包就是函數+詞法環境前端

myfunc 函數被調用時,變量 count 依然是可用的,也能夠更新的git

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

var addFun1 = add(4)
var addFun2 = add(9)

console.log(addFun1(2)) //6
console.log(addFun2(2))  //11

add 接受一個參數 x ,返回一個函數,它的參數是 y ,返回 x+y github

add 是一個函數工廠,傳入一個參數,就能夠建立一個參數和其餘參數求值的函數。面試

addFun1addFun2 都是閉包。他們使用相同的函數定義,但詞法環境不一樣, addFun1x4 ,後者是 5 算法

即:數組

  • 閉包能夠訪問當前函數之外的變量
  • 即便外部函數已經返回,閉包仍能訪問外部函數定義的變量與參數
  • 閉包能夠更新外部變量的值

因此,閉包能夠:緩存

  • 避免全局變量的污染
  • 可以讀取函數內部的變量
  • 能夠在內存中維護一個變量

使用閉包應該注意什麼

  • 代碼難以維護: 閉包內部是能夠訪問上級做用域,改變上級做用域的私有變量,咱們使用的使用必定要當心,不要隨便改變上級做用域私有變量的值
  • 使用閉包的注意點: 因爲閉包會使得函數中的變量都保存在內存中,內存消耗很大,因此不能濫用閉包,不然會形成網頁的性能問題,在IE中可能致使內存泄漏。解決方法是,在退出函數以前,將不使用的局部變量所有刪除(引用設置爲 null ,這樣就解除了對這個變量的引用,其引用計數也會減小,從而確保其內存能夠在適當的時機回收)
  • 內存泄漏: 程序的運行須要內存。對於持續運行的服務進程,必須及時釋放再也不用到的內存,不然佔用愈來愈高,輕則影響系統性能,重則致使進程崩潰。再也不用到的內存,沒有及時釋放,就叫作內存泄漏
  • this指向: 閉包的this指向的是window

應用場景

閉包一般用來建立內部變量,使得這些變量不能被外部隨意修改,同時又能夠經過指定的函數接口來操做。例如 setTimeout 傳參、回調、IIFE、函數防抖、節流、柯里化、模塊化等等閉包

setTimeout 傳參

//原生的setTimeout傳遞的第一個函數不能帶參數
setTimeout(function(param){
    alert(param)
},1000)


//經過閉包能夠實現傳參效果
function myfunc(param){
    return function(){
        alert(param)
    }
}
var f1 = myfunc(1);
setTimeout(f1,1000);

回調

大部分咱們所寫的 JavaScript 代碼都是基於事件的 — 定義某種行爲,而後將其添加到用戶觸發的事件之上(好比點擊或者按鍵)。咱們的代碼一般做爲回調:爲響應事件而執行的函數。

例如,咱們想在頁面上添加一些能夠調整字號的按鈕。能夠採用css,也可使用:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>test</title>
    <link rel="stylesheet" href="">
</head>
<style>
    body{
        font-size: 12px;
    }
    h1{
        font-size: 1.5rem;
    }
    h2{
        font-size: 1.2rem;
    }
</style>
<body>
  
    <p>測試</p>

    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>

<script>
    function changeSize(size){
        return function(){
            document.body.style.fontSize = size + 'px';
        };
    }

    var size12 = changeSize(12);
    var size14 = changeSize(14);
    var size16 = changeSize(16);

    document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
</script>
</body>
</html>

IIFE

var arr = [];
    for (var i=0;i<3;i++){
      //使用IIFE
      (function (i) {
        arr[i] = function () {
          return i;
        };
      })(i);
    }
    console.log(arr[0]()) // 0
    console.log(arr[1]()) // 1
    console.log(arr[2]()) // 2

函數防抖、節流

debouncethrottle 是開發中經常使用的高階函數,做用都是爲了防止函數被高頻調用,換句話說就是,用來控制某個函數在必定時間內執行多少次。

使用場景

好比綁定響應鼠標移動、窗口大小調整、滾屏等事件時,綁定的函數觸發的頻率會很頻繁。若稍處理函數微複雜,須要較多的運算執行時間和資源,每每會出現延遲,甚至致使假死或者卡頓感。爲了優化性能,這時就頗有必要使用 debouncethrottle了。

debounce throttle 區別

防抖 (debounce) :屢次觸發,只在最後一次觸發時,執行目標函數。

節流(throttle):限制目標函數調用的頻率,好比:1s內不能調用2次。

源碼實現

debounce

// 這個是用來獲取當前時間戳的
function now() {
  return +new Date()
}
/**
 * 防抖函數,返回函數連續調用時,空閒時間必須大於或等於 wait,func 纔會執行
 *
 * @param  {function} func        回調函數
 * @param  {number}   wait        表示時間窗口的間隔
 * @param  {boolean}  immediate   設置爲ture時,是否當即調用函數
 * @return {function}             返回客戶調用函數
 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延遲執行函數
  const later = () => setTimeout(() => {
    // 延遲函數執行完畢,清空緩存的定時器序號
    timer = null
    // 延遲執行的狀況下,函數會在延遲函數中執行
    // 使用到以前緩存的參數和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 這裏返回的函數是每次實際調用的函數
  return function(...params) {
    // 若是沒有建立延遲執行函數(later),就建立一個
    if (!timer) {
      timer = later()
      // 若是是當即執行,調用函數
      // 不然緩存參數和調用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延遲執行函數(later),調用的時候清除原來的並從新設定一個
    // 這樣作延遲函數會從新計時
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}

throttle

/**
 * underscore 節流函數,返回函數連續調用時,func 執行頻率限定爲 次 / wait
 *
 * @param  {function}   func      回調函數
 * @param  {number}     wait      表示時間窗口的間隔
 * @param  {object}     options   若是想忽略開始函數的的調用,傳入{leading: false}。
 *                                若是想忽略結尾函數的調用,傳入{trailing: false}
 *                                二者不能共存,不然函數不能執行
 * @return {function}             返回客戶調用函數
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的時間戳
    var previous = 0;
    // 若是 options 沒傳則設爲空對象
    if (!options) options = {};
    // 定時器回調函數
    var later = function() {
      // 若是設置了 leading,就將 previous 設爲 0
      // 用於下面函數的第一個 if 判斷
      previous = options.leading === false ? 0 : _.now();
      // 置空一是爲了防止內存泄漏,二是爲了下面的定時器判斷
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到當前時間戳
      var now = _.now();
      // 首次進入前者確定爲 true
      // 若是須要第一次不執行函數
      // 就將上次時間戳設爲當前的
      // 這樣在接下來計算 remaining 的值時會大於0
      if (!previous && options.leading === false) previous = now;
      // 計算剩餘時間
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是當前調用已經大於上次調用時間 + wait
      // 或者用戶手動調了時間
      // 若是設置了 trailing,只會進入這個條件
      // 若是沒有設置 leading,那麼第一次會進入這個條件
      // 還有一點,你可能會以爲開啓了定時器那麼應該不會進入這個 if 條件了
      // 其實仍是會進入的,由於定時器的延時
      // 並非準確的時間,極可能你設置了2秒
      // 可是他須要2.2秒才觸發,這時候就會進入這個條件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定時器就清理掉不然會調用二次回調
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判斷是否設置了定時器和 trailing
        // 沒有的話就開啓一個定時器
        // 而且不能不能同時設置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

柯里化

在計算機科學中,柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。這個技術由 Christopher Strachey 以邏輯學家 Haskell Curry 命名的,儘管它是 Moses Schnfinkel 和 Gottlob Frege 發明的。

var add = function(x) {
  return function(y) {
    return x + y;
  };
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3

這裏定義了一個 add 函數,它接受一個參數並返回一個新的函數。調用 add 以後,返回的函數就經過閉包的方式記住了 add 的第一個參數。因此說 bind 自己也是閉包的一種使用場景。

柯里化是將 f(a,b,c) 能夠被以 f(a)(b)(c) 的形式被調用的轉化。JavaScript 實現版本一般保留函數被正常調用和在參數數量不夠的狀況下返回偏函數這兩個特性。

模塊化

模塊化的目的在於將一個程序按照其功能作拆分,分紅相互獨立的模塊,以便於每一個模塊只包含與其功能相關的內容,模塊之間經過接口調用

模塊化開發和閉包息息相關,經過模塊模式須要具有兩個必要條件能夠看出:

  • 外部必須是一個函數,且函數必須至少被調用一次(每次調用產生的閉包做爲新的模塊實例)
  • 外部函數內部至少有一個內部函數, 內部函數用於修改和訪問各類內部私有成員
function myModule (){
    const moduleName = '個人自定義模塊'
    var name = 'sisterAn'

    // 在模塊內定義方法(API)
    function getName(){
        console.log(name)
    }
    function modifyName(newName){
        name = newName
    }

    // 模塊暴露:  向外暴露API
    return {
        getName,
        modifyName
    }
}

// 測試
const md = myModule()
md.getName()    // 'sisterAn'
md.modifyName('PZ')
md.getName()    // 'PZ'

// 模塊實例之間互不影響
const md2 = myModule()
md2.sayHello = function () {
    console.log('hello')
}
console.log(md) // {getName: ƒ, modifyName: ƒ}

常見錯誤

在循環中建立閉包

var data = []

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

data[0]()   // 3
data[1]()   // 3
data[2]()   // 3

這裏的 i 是全局下的 i,共用一個做用域,當函數被執行的時候這時的 i=3,致使輸出的結構都是3

方案一:閉包

var data = []

function myfunc(num) {
  return function(){
    console.log(num)
  }
}

for (var i = 0; i < 3; i++) {
  data[i] = myfunc(i)
}

data[0]()   // 0
data[1]()   // 1
data[2]()   // 2

方案二:let

若是不想使用過多的閉包,你能夠用 ES6 引入的 let 關鍵詞:

var data = []

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

data[0]()   // 0
data[1]()   // 1
data[2]()   // 2

方案三:forEach

若是是數組的遍歷操做(以下例中的 arr ),還有一個可選方案是使用 forEach()來遍歷:

var data = []

var arr = [0, 1, 2]
arr.forEach(function (i) {
  data[i] = function () {
    console.log(i)
  }
})

data[0]()   // 0
data[1]()   // 1
data[2]()   // 2

最後

本文首發自「三分鐘學前端」,天天三分鐘,進階一個前端小 tip

面試題庫
算法題庫
相關文章
相關標籤/搜索