前端必會-閉包

前言

不少同窗都有過,面試的時候被問到閉包,一時間不知道從哪裏提及的狀況。 其實閉包只是 js 的一種現象(或者說特性),沒有想象中的那麼可怕。前端

什麼是閉包

函數及函數對其詞法環境的引用共同組成閉包。 閉包讓咱們從內部函數訪問外部函數的做用域。vue

那麼什麼是詞法環境呢?react

詞法環境

詞法環境理解爲函數在定義時肯定的做用域環境。 舉個例子:面試

function a() {
  var i = 0;
  return function inner() {
    i++;
    return i;
  };
}
// 或者這樣
function a() {
  var i = 0;
  function inner() {
    return i;
  }
}
複製代碼

內層函數的詞法環境就是變量 i 所在的環境。chrome

閉包的 「閉」 理解爲:
內層函數所能訪問的做用域是在聲明的時候肯定的,而不是調用的時候,其詞法做用域是對外閉合的。數組

閉包的 「包」 理解爲:
內層函數保留對其詞法環境的引用,就像是身上揹着一個小揹包,這個揹包裏裝着對其詞法環境中引用的變量,內層函數訪問 i 時,在局部做用域中找不到時,就會到揹包裏看看。markdown

閉包的做用

  • 閉包使函數的私有變量不受外部干擾閉包

  • 是變量存於內存中不被銷燬app

舉個例子:dom

function a() {
  var i = 0;
  return function () {
    i++;
    return i;
  };
}
var y = a();
y(); // 1
y(); // 2
y(); // 3
複製代碼

函數 a 中的變量 i 不能在函數 a 以外被訪問。

  • 閉包中的變量 i 保存在哪裏?
    保存在父做用域中,每次訪問函數 y 時, 在函數 y 中找不到變量 i, 會順着做用域鏈一直向上找,直到全局做用域中也沒找到爲止。

閉包的應用場景

閉包常被見於實現單例模式、柯里化、防抖、節流、模塊化

2-0 單例模式

實例僅建立一次、避免重複建立帶來的內存消耗

function singleIns(name) {
  this.name = name;
}
singleIns.getInstance = (function () {
  var instance = null;
  return function (name) {
    if (!this.instance) {
      this.instance = new singleIns(name);
    }
    return this.instance;
  };
})();
var a = singleIns.getInstance("a");
var b = singleIns.getInstance("b");
a === b; // true
複製代碼

2-1 柯里化

入參可拆解後傳入

function curry(func) {
  return function curried(...args) {
    if (args.length >= func.length) {
      func.apply(this, args);
    } else {
      return function (...args2) {
        curried.apply(this, args.concat(args2));
      };
    }
  };
}
// example
function a(x, y, z) {
  console.log(x + y + z);
}
var b = curry(a);
b(1, 2, 3); // 6
b(1, 2)(3); // 6
b(1)(2, 3); // 6
b(1)(2)(3); // 6
複製代碼

2-2 防抖

一段時間內一直觸發僅執行一次

// 僅執行最後一次
function debounce(func, time) {
  let timer = null;
  return function (...args) {
    timer && clearTimeout(timer);
    setTimeout(function () {
      func.apply(this, args);
    }, time);
  };
}
// 僅執行第一次
function debounce(func, time){
  let timer = null;
  func.id = 0;
  return function(...args){
    if(func.id !== 0){
      clearTimeout(timer;)
    }
    timer = setTimeout(function () {
      func.id = 0;
      func.apply(this, args);
    }, time);
    func.id = func.id + 1;
  }
}
複製代碼

2-3 節流

一段時間內一直觸發,每隔固定時間內觸發一次

// 時間戳方式
function throttle(func, time) {
  let start = new Date();
  return function (...args) {
    if (new Date() - start >= time) {
      func.apply(this, args);
      start = new Date();
    }
  };
}
複製代碼

2-4 模塊化

模塊化封裝中將內部邏輯封裝在模塊內,將方法暴露到模塊外。

function module() {
  var pool = [];
  function add(item) {
    pool.push(item);
    return pool;
  }
  function remove(num) {
    pool.forEach((item, index) => {
      if (item === num) {
        pool.splice(index, 1);
      }
    });
    return pool;
  }
  return {
    add: add,
    remove: remove,
  };
}
var foo = module();
foo.add();
foo.remove();
複製代碼

仔細觀察上面的例子是否是很容易理解閉包啦?

閉包的壞處

濫用閉包可能會形成內存泄漏(無用變量存於內存中沒法回收,一直佔用內存)。解決此問題的方法是,清除變量(設爲 null)。

function a() {
  var i = 0;
  return function () {
    i++;
    return i;
  };
}
var y = a();
y();
y = null; // 清除變量後,引用消失,閉包就不存在了
複製代碼

常見面試題

    1. 考察輸出結果
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
複製代碼

輸出結果爲:連續輸出 5 個 5 請修改以上代碼,使其輸出 0、一、二、三、4

方法一: 使用 setTimeout 的第三個參數

for (let i = 0; i < 5; i++) {
  setTimeout(
    function () {
      console.log(i);
    },
    0,
    i
  );
}
複製代碼

方法二:使用 let

for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
複製代碼

let 建立了塊級做用域。

方法三:使用自執行函數

for (var i = 0; i < 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i);
    }, 0);
  })(i);
}
複製代碼

自執行函數將 i 傳遞進入一個新的執行上下文。

方法四: 藉助函數參數

function output(i) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
for (var i = 0; i < 5; i++) {
  output(i);
}
複製代碼

函數傳參是按值傳遞。

方法五:借用 async

const timeout = function (time) {
  return new Promise((resolve, reject) => {
    setTimeout(function () {
      resolve();
    }, time);
  });
};
async function print() {
  for (var i = 0; i < 5; i++) {
    await timeout(1000);
    console.log(i);
  }
}
await print();
複製代碼
  1. 考察輸出結果
var a = 100;
function create() {
  var a = 200;
  return function () {
    console.log(a);
  };
}
var fn = new create();
fn();
複製代碼

這題很簡單,就是閉包概念的體現,輸出 200,這裏再也不解釋啦。

  1. 實現一個與 sum(x,y)功能相同的 sum(x)(y)函數
function sum(a) {
  return function (b) {
    return a + b;
  };
}
sum(1)(2); // 3
複製代碼

考察的是柯里化的概念

tips

是否是全部的閉包都須要手動的清除?

有用的閉包不須要清除,無用的閉包才須要清除。
若是是 dom 事件中的閉包,vue、react 中組件擁有生命週期,卸載時會自動解除 dom 上綁定的事件,內存會自動回收。

開發時不要使用閉包?

閉包無處不在,每當咱們模擬私有變量的時候,閉包就已經產生了。不要讓 length 太長的對象和數組一直存於內存當中,合理的使用閉包便可。

如何排查內存泄漏?

閉包並非致使內存泄漏的惟一緣由,藉助 chrome 開發者工具(devTools) 的 perfomance 工具 和 memory 工具能夠詳細觀察內存狀況。這篇不是講內存泄漏的,再也不贅述使用方法,推薦學習devtools 官方文檔

總結

閉包做爲前端八股文之一,難倒了不少正在找工做的同窗。小姐姐如今也在一個學習的過程當中,可能有不許確、不正確的地方,歡迎你們點贊、討論,一塊兒進步呀。但願對一些同窗有幫助。

本文章爲【js 基礎】系列文章,關注小姐姐,一塊兒學一學。

相關文章
相關標籤/搜索