爲了前端的深度-閉包概念與應用

總結

定義:閉包可讓一個函數訪問並操做其聲明時的做用域中的變量和函數,而且,即便聲明時的做用域消失了,也能夠調用javascript

應用:java

  1. 私有變量
  2. 回調與計時器
  3. 綁定函數上下文
  4. 偏應用函數
  5. 函數重載:緩存記憶、函數包裝
  6. 即時函數:獨立做用域、簡潔代碼、循環、類庫包裝、經過參數限制做用域內的名稱

前言

最近忙着公司的項目,沒有時間去繼續面試受虐,只抽空讀了一遍《javascript 忍者祕籍》。jquery

今天晚上有點焦慮失眠,就乾脆寫一篇本身總結的閉包知識。面試

內容基本所有來自忍者祕籍,以爲寫的好的話,能夠仔細再看一遍書;以爲寫的很差的,多是由於我理解不到位,致使文中本身思考的地方出了差錯,也多是我省略了書中的按部就班,致使漏掉一些知識點。各類緣由,都請指正。緩存

正文

看了不少文章,都在說閉包的定義和閉包的優缺點。我呢,再加上閉包的應用吧。bash

閉包的定義不少文章裏都有,我記得有一種角度說只要能訪問外部變量的就是閉包,還有一種角度全部函數都是閉包。閉包

我以爲這些回答是正確的,可是不太方便麪試官繼續問下去,或者說是很差引導面試官。因此,若是是我在面試,我會用忍者祕籍裏的定義:閉包是一個函數在建立時容許該自身函數訪問並操做該自身函數以外的變量時所建立的做用域。這個還有點繞口,更清晰的版本是:閉包可讓一個函數訪問並操做其聲明時的做用域中的變量和函數,而且,即便聲明時的做用域消失了,也能夠調用。要注意的是:閉包不是在建立的那一時刻點的狀態的快照,而是一個真實的封裝,只要閉包存在,就能夠對其進行修改。app

最簡單的閉包:異步

// 全局做用於就是一個閉包
var outerVal = 'lionel';
function outerFn(){
  console.log(outerVal)
}
outerFn() // lionel
複製代碼

複雜點的,也是咱們印象中的:函數

var outerVal = 'lionel';
var later;
function outerFn(){
  var innerVal = 'karma';
  function innerFn(){
    console.log(outerVal, innerVal);
  }
  later = innerFn;
}
outerFn();  // 此時outerFn的做用域已經消失了
later();  // lionel karma
複製代碼

難以理解的,這個例子咱們能夠理解到,閉包不是快照:

var later;
function outerFn(){
  function innerFn(){
    console.log(lateVal)
  }
  later = innerFn;
}
console.log(lateVal); // undefined
var lateVal = 'lionel'; // 變量提高,閉包聲明的那一刻存在這個變量
outerFn();
later(); // lionel
複製代碼

缺點你們很熟悉了,閉包裏的信息會一直保存在內存裏。解決方法是,在你以爲能夠的地方,清除引用,像上面的例子中,使用 later = null 便可,這樣就能夠在下次垃圾回收中,清除閉包。

下面咱們重點來看一下閉包的實際應用

1、私有變量

閉包常見的用法,封裝私有變量。用戶沒法直接獲取和修改變量的值,必須經過調用方法;而且這個用法能夠建立只讀的私有變量哦。咱們從下面的例子來理解:

function People(num) { // 構造器
  var age = num;
  this.getAge = function() {
    return age;
  };
  this.addAge = function() {
    age++;
  };
}
var lionel = new People(23); // new方法會固化this爲lionel哦
lionel.addAge();
console.log(lionel.age);      // undefined
console.log(lionel.getAge()); // 24
var karma = new People(20);
console.log(karma.getAge()); // 20
複製代碼

以下圖,lionel中並不存在age屬性,age只存在new的那個過程的做用域中,而且,getAge和addAge中,咱們能夠看到他們的做用域中都包含一個People的閉包。

alt

2、回調和計時器

這部分我沒有多聊的,

3、綁定函數上下文

剛看到這個應用可能有點懵,仔細想一想其實咱們看到不少次了,那就是bind()函數的實現方式,這裏再貼一次簡單實現的代碼:

Function.prototype.myBind = function() {
  var fn = this,
      args = [...arguments],
      object = args.shift();
  return function() {
    return fn.apply(object, args.concat(...arguments))
  }
}
複製代碼

這裏要注意的是:bind()並非apply和call的替代方法。該方法的潛在目的是經過匿名函數和閉包控制後續執行上下文。

4、偏應用函數

偏應用函數返回了一個含有預處理參數的函數,以便後期能夠調用。具體仍是看代碼吧

Function.prototype.partial = function() {
  var fn = this,
      args = [...arguments];
  return function() {
    var arg = 0;
    var argsTmp = [...args]
    for (var i=0; i<argsTmp.length && arg < arguments.length; i++) {
      if (argsTmp[i] === undefined) {
        argsTmp[i] = arguments[arg++]
      }
    }
    return fn.apply(this, argsTmp)
  }
}
function addAB(a ,b) {
  console.log( a + b);
}
var hello = addAB.partial('hello ', undefined);
hello('lionel'); // hello lionel
hello('karma'); // hello karma
var bye = addAB.partial(undefined, ' bye')
bye('lionel'); // lionel bye
bye('karma'); // karma bye
複製代碼

上面的例子可能有點難以理解,下面是一個簡化版的例子:

function add(a) {
  return function(b) {
    console.log( a + b);
  };
}
var hello = add('hello ')
hello('lionel'); // hello lionel
hello('karma'); // hello karma
複製代碼

emmm... 寫到這裏去研究了半天柯里化和偏函數的區別,最終找到一篇文章符合個人想法:偏函數與函數柯里化,不對的地方請指正。

5、函數重載

1 緩存記憶

咱們能夠經過閉包來包裝一個函數,,從而讓調用咱們函數的人,不知道咱們採用了緩存的方法,或者說,不須要調用者額外作什麼,就能夠緩存計算結果,以下代碼

Function.prototype.memoized = function(key) {
  this._values = this._values || {};
  return this._values[key] !== undefined ?
    this._values[key] + ' memoized' :
    this._values[key] = this.apply(this, arguments);
}
Function.prototype.memoize = function() {
  var fn = this;
  return function() {
    // return fn.memoized.apply(fn, arguments);
    console.log(fn.memoized.apply(fn, arguments))
  }
}
var computed = (function(num){
  // 這裏有超級超級複雜的計算,耗時特別久
  console.log('----計算了好久-----')
  return 2
}).memoize();
computed(1); // ----計算了好久-----     2
computed(1); // 2 memoized
複製代碼

2 函數包裝

下面的這個例子寫的沒有書裏的好。

function wrap(object, method, wrapper){
  var fn = object[method];
  return object[method] = function() {
    return wrapper.apply(this, [fn.bind(this)].concat(...arguments))
  }
}
let util = {
  reciprocal: function(tag){
    console.log(1 / tag)
  }
}

wrap(util, 'reciprocal', function(original, tag){
   return tag == 0 ? 0 : original(tag)
})

util.reciprocal(0);  // 0
複製代碼

6、即時函數

針對爲何即時函數會放在閉包裏介紹,下圖是一個很好的說明:

alt

1 獨立做用域

var button = $('#mybtn');
(function(){
  var numClicks = 0;
  button.click = function(){
    alert(++numClicks)
  }
})
複製代碼

2 簡潔代碼

// 例若有以下data
data = {
  a: {
    b: {
      c: {
        get: function(){},
        set: function(){},
        add: function(){}
      }
    }
  }
}
// 第一種調用這三個方法的代碼以下, 繁瑣
data.a.b.c.get();
data.a.b.c.set();
data.a.b.c.add();
// 第二種方法以下, 引入多餘變量
var short = data.a.b.c;
short.get();
short.set();
short.add();
// 第三種使用即時函數 優雅
(function(short){
  short.get();
  short.set();
  short.add();
})(data.a.b.c)
複製代碼

3 循環

這部分是經典的for循環中調用setTimeout打印i,之因此打印i爲固定值,是由於閉包並非快照,而是變量的引用,在執行到異步隊列時,i已經改變。

解決方法就是再用一個閉包和即時函數。

4 類庫包裝

// 下方的代碼展現了,爲何jquery庫中,它能夠放心的用jquery而不擔憂這個變量被替換
(function(){
  var jQuery = window.jQuery = function() {
    // Initialize
  };
  // ...
})()
複製代碼

5 經過參數限制做用域內的名稱

// 當咱們擔憂jquery中的$符號,被其餘庫佔用,致使咱們代碼出問題的時候,
// 用下面的方法,就能夠放心大膽的用啦(不過要注意:若是jQuery也被佔用的話就...)
(function($){
  $.post(...)
})(jQuery)
複製代碼
相關文章
相關標籤/搜索