3. JavaScript 閉包和高階函數(shù)

本文源于本人關(guān)于《JavaScript設(shè)計(jì)模式與開發(fā)實(shí)踐》(曾探著)的閱讀總結(jié)。想詳細(xì)了解具體內(nèi)容建議閱讀該書。

1. 閉包

首先我們需要了解變量的作用域以及變量的生存周期。

1.1 變量的作用域

  • 在函數(shù)中聲明的變量作用域 —— 局部變量。
  • 未在任何函數(shù)中聲明的變量 —— 全局變量。
  • 如果在函數(shù)中未用var等聲明變量,這個變量就會變成全局變量。
  • 函數(shù)外不能訪問函數(shù)內(nèi)的變量,函數(shù)內(nèi)可以訪問函數(shù)外的變量。
var a = 1;
var func = function(){
    var b = 2;
    var func2 = function(){
        var c = 3;
        console.log(b); // 2
        console.log(a); // 1
    }
    console.log(c); // undefined
}

func();
console.log(b); // undefined

1.2 變量的生存周期

對于存在函數(shù)內(nèi)用var聲明的局部變量而言,當(dāng)函數(shù)推出時,這些局部變量就失去了它們的價值,它們都會隨著函數(shù)調(diào)用的結(jié)束而被銷毀。

現(xiàn)在看看這段代碼:

var func = function() {
    var a = 0;
    return function(){
        console.log(a++);
    }
}

var f = func();

f() // 0
f() // 1
f() // 2
f() // 3

跟我們之前的推論相反,函數(shù)退出后,局部變量a并沒有消失。這是因?yàn)閳?zhí)行f=func()時,f返回了一個匿名函數(shù)的引用,它可以訪問到func被調(diào)用時產(chǎn)生的環(huán)境,而局部變量a一直處于這個環(huán)境里。既然局部變量所在環(huán)境還能被外界訪問,這個變量就有了不被銷魂的理由。在這里產(chǎn)生了一個閉包結(jié)構(gòu)。

var Type = (function () {
  var Type = {};
  for (var i = 0, type; type = ['String', 'Number', 'Array'][i++];) {
    (function (type) {
      Type['is' + type] = function (obj) {
        return Object.prototype.toString.call(obj) === '[object ' + type + ']';
      }
    })(type)
  }
  return Type;
})();

console.log(Type.isArray([]));
console.log(Type.isString(''));

利用閉包保存每次傳入的type。

1.3 閉包作用

封裝變量

計(jì)算階乘:

var mult = function () {
  var a = 1;
  for (var i = 0, l = arguments.length; i < l; i++) {
    a = a * arguments[i];
  }
  return a;
}

console.log(mult(1, 2, 3, 4, 5, 5, 5));

var mult_closure = (function () {
  var cache = {};
  return function () {
    var args = Array.prototype.join.call(arguments, ',');
    if (args in cache) {
      return cache[args];
    }
    var a = 1;
    for (var i = 0, l = arguments.length; i < l; i++) {
      a = a * arguments[i];
    }
    return cache[args] = a;
  }
})()

t1 = new Date();  
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t1);  // 花費(fèi)4ms
t2 = new Date();
console.log(mult_closure(1, 2, 3, 4, 5, 5, 5));
console.log(new Date() - t2);  // 花費(fèi)0ms

第二個用了閉包封裝了緩存,當(dāng)計(jì)算相同參數(shù)時,可以直接返回結(jié)果。

閉包合面向?qū)ο笤O(shè)計(jì)

閉包能夠?qū)崿F(xiàn)的,面向?qū)ο笠部梢詫?shí)現(xiàn),反之依然。

閉包:

var extent = function(){
    var value = 0;
    return {
        call: function(){
            console.log(value++);
        }
    }
}

var extent = extent();
extent.call(); // 0
extent.call(); // 1
extent.call(); // 2

面向?qū)ο螅?/p>

var extent = {
    value: 0,
    call: function(){
        console.log(this.value++);
    }
}

extent.call(); // 0
extent.call(); // 1
extent.call(); // 2
閉包實(shí)現(xiàn)命令模式

命令模式意圖把請求封裝為對象, 從而分離請求的發(fā)起者和請求的接受者。在命令被執(zhí)行前預(yù)先植入命令的接收者。

    var Tv = {
      open: function () {
        console.log('open');
      },
      close: function () {
        console.log('close');
      }
    }

    var createCommand = function (receiver) {
      var execute = function () {
        receiver.open();
      },
        undo = function () {
          receiver.close();
        }
      return {
        execute: execute,
        undo: undo
      }
    }

    var setCommond = function (commond) {
      document.getElementById('execute').onclick = commond.execute;
      document.getElementById('undo').onclick = commond.undo;
    }

    setCommond(createCommand(Tv));

把命令封存在了createCommond中。

高階函數(shù)

  • 函數(shù)可以作為參數(shù)被傳遞
  • 函數(shù)可以作為返回值輸出
函數(shù)作為參數(shù)傳遞
  • 回調(diào)函數(shù)
var getUserInfo = function(userId, callback){
    $.ajax('http://xxx.com/getUserInfo?' + userId, function({
      if(typeof callback === 'function'){
        callback(data)
      }
    })
}

getUserInfo(123, function(data){
    console.log(data.userName);
})

該例子和第一章的一致, 把變化的地方抽離了出來,以回調(diào)函數(shù)的形式傳入。

  • Array.prototype.sort:接受一個函數(shù)作為參數(shù)。這個函數(shù)里面封裝了數(shù)組元素的排序規(guī)則。我們目的對數(shù)組進(jìn)行排序,這是不變的部分,而使用什么規(guī)則去排序是可變的部分。
函數(shù)作為返回值輸出
  • 判斷數(shù)據(jù)類型:
var Type = (function () {
  var Type = {};
  for (var i = 0, type; type = ['String', 'Number', 'Array'][i++];) {
    (function (type) {
      Type['is' + type] = function (obj) {
        return Object.prototype.toString.call(obj) === '[object ' + type + ']';
      }
    })(type)
  }
  return Type;
})();

console.log(Type.isArray([]));
console.log(Type.isString(''));

這個函數(shù)把返回的函數(shù)都賦予給了Type對象,返回的都是函數(shù),故也為高階函數(shù)。

  • getSingle:單例模式
var getSingle = function (fn) {
  var ret;
  return function () {
    return ret || (ret = fn.apply(this, arguments));
  }
}

var getObj = getSingle(function () {
  return {};
})

var obj1 = getObj();
var obj2 = getObj();

console.log(obj1 === obj2); // true

既把函數(shù)作為了參數(shù)也作為了返回值,利用了閉包保存了一個單一不變的飲用,如果存在引用則直接返回,不存在才調(diào)用函數(shù)創(chuàng)建單例。

高階函數(shù)實(shí)現(xiàn)AOP

AOP指面向切面編程,主要作用把一些跟核心業(yè)務(wù)邏輯模塊無關(guān)的功能抽離出來,再通過“動態(tài)織入”的方式滲入業(yè)務(wù)模塊中。通常js中實(shí)現(xiàn)AOP都是只把一個函數(shù)織入到另一個函數(shù)之中,實(shí)現(xiàn)方法之一:

Function.prototype.before = function (fn) {
  var _self = this; // 保存原函數(shù)的引用
  return function () { // 返回了包含原函數(shù)和新函數(shù)的代理函數(shù)
    fn.apply(this, arguments);
    return _self.apply(this, arguments); // 執(zhí)行原函數(shù)
  }
}

Function.prototype.after = function (fn) {
  var _self = this; // 保存原函數(shù)的引用
  return function () {
    var ret = _self.apply(this, arguments);
    fn.apply(this, arguments);
    return ret;
  }
}

var func = function () {
  console.log(2);
}

func = func.before(function () {
  console.log(1);
}).after(function () {
  console.log(3);
})

func(); // 1, 2, 3
其他應(yīng)用
  • currying:柯里化,函數(shù)首先會接受一些參數(shù),接受了參數(shù)并不會馬上求值,而是繼續(xù)返回另一個函數(shù),待真正需要求值時,之前傳入的所有參數(shù)都會被計(jì)算。
function curring(fn) {
  var args = []; // 保存不計(jì)算的值

  return function () {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      [].push.apply(args, arguments);
    }
  }
}

var cost = (function () {
  var money = 0; // 用于記錄累加的值

  return function () {
    for (var i = 0, l = arguments.length; i < l; i++) {
      money += arguments[i];
    }
    return money;
  }
})()

var cost = curring(cost);
cost(100);
cost(200);
cost(300);
console.log(cost()); // 600
  • uncurrying:我們通??梢允褂胏all和apply去借用其他對象的方法, 但是有沒有辦法把泛化this的過程提取出來呢?以下代碼是uncurrying的實(shí)現(xiàn)方式之一:
Function.prototype.uncurrying = function () {
  var self = this;
  return function () {
    var obj = Array.prototype.shift.call(arguments);
    return self.apply(obj, arguments);
  }
}

for (var i = 0, fn, ary = ['push', 'shift', 'forEach']; fn = ary[i++];) {
  Array[fn] = Array.prototype[fn].uncurrying();
};

var obj = {
  length: 3,
  0: 1,
  1: 2,
  2: 3
};

Array.push(obj ,4);
console.log(obj.length); // 4

通過uncurrying的方式,Array的push就變成了一個普通的push函數(shù),這樣push函數(shù)的作用也與Array的效果一樣。同樣不僅僅局限于操作array對象,其他對象也可以。

  • 函數(shù)節(jié)流:函數(shù)被頻繁調(diào)用 ,嚴(yán)重影響性能:
    • window.onresize
    • mousemove事件
    • 上傳進(jìn)度

節(jié)流原理:比如window.onresize,我們在改變窗口大小時,打印窗口大小的工作1s進(jìn)行了10次,而我們實(shí)際上只需要2次或者3次,這就需要我們按時間段來忽略掉一些事件請求。

    var throttle = function (fn, interval) {
      var _self = fn, // 保存所需被延遲執(zhí)行的函數(shù)引用
        timer, // 定時器
        firstTime = true; // 是否第一次被調(diào)用

      return function () {
        var args = arguments,
          _me = this;

        if (firstTime) {
          _self.apply(_me, args);
          return firstTime = false;
        }

        if (timer) {
          return false; // 定時器還在 說明還在上一次執(zhí)行后的暫停時間中
        } else {
          timer = setTimeout(function () {
            clearTimeout(timer);
            timer = null;
            _self.apply(_me, args);
          }, interval || 500);
        }
      }
    }

    window.onresize = throttle(function () {
      console.log(1);
    }, 500);
  • 分時函數(shù): 比如qq渲染好友列表,一次性渲染1000個好友會讓瀏覽器吃不消,故可以改為每隔200毫秒創(chuàng)建8個節(jié)點(diǎn)。
    var timeChunk = function (ary, fn, count) {
      var obj, t;
      var len = ary.length;

      var start = function () {
        for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
          var obj = ary.shift();
          fn(obj);
        }
      }

      return function () {
        t = setInterval(function () {
          if (ary.length === 0) {
            return clearInterval(t);
          }
          start();
        }, 200);
      };
    };

    var ary = [];
    for (var i = 1; i <= 1000; i++) {
      ary.push(i);
    }

    var renderFriendList = timeChunk(ary, function (n) {
      var div = document.createElement('div');
      div.innerHTML = n;
      document.body.appendChild(div);
    }, 8)

    renderFriendList();
  • 惰性加載函數(shù):比如判斷瀏覽器類型而采用不同的添加事件方法,如果每次執(zhí)行添加事件時都要判斷,那么效率很低,因?yàn)楫吘古袛噙^了一次之后,代碼一直都是在相同的環(huán)境中運(yùn)行。
    • 解決辦法:提前判斷瀏覽器類型,并選定使用的方法。
    • 缺點(diǎn):如果我們整個業(yè)務(wù)下來,并沒有用到事件添加函數(shù),那么這次計(jì)算就白計(jì)算了。
    • 最終:調(diào)用了 事件添加函數(shù)后,才進(jìn)行第一次判斷,并且利用這個判斷的返回值,以后不再繼續(xù)做判斷。
    var addEvent = function (elem, type, handler) {
      if (window.addEventListener) {
        addEvent = function (elem, type, handler) {
          elem.addEventListener(type, handler, false);
        }
      } else if (window.attachEvent) {
        addEvent = function (elem, type, handler) {
          elem.attachEvent('on' + type, handler);
        }
      }

      addEvent(elem, type, handler);
    }

第一次調(diào)用后,根據(jù)瀏覽器類型選定一個固定的事件添加函數(shù)賦值給addEvent。 之后就一直使用該方法調(diào)用,如果整個流程不使用到事添加函數(shù),則不發(fā)生判斷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容