你要看看這些有趣的函數方法嗎?

前言

這是underscore.js源碼分析的第六篇,若是你對這個系列感興趣,歡迎點擊javascript

underscore-analysis/ watch一下,隨時能夠看到動態更新。css

下劃線中有很是多頗有趣的方法,能夠用比較巧妙的方式解決咱們平常生活中遇到的問題,好比_.after,_.before_.defer...等,也許你已經用過他們了,今天咱們來深刻源碼,一探究竟,他們究竟是怎麼實現的。java

function

指定調用次數(after, before)

把這兩個方法放在前面也是由於他們倆可以解決咱們工做中至少如下兩個問題git

  1. 若是你要等多個異步請求完成以後纔去執行某個操做fn,那麼你能夠用_.after,而沒必要寫多層異步回調地獄去實現需求github

  2. 有一些應用可能須要進行初始化操做並且僅須要一次初始化就能夠,通常的作法是在入口處對某個變量進行判斷,若是爲真那麼認爲已經初始化過了直接return掉,若是爲假那麼進行參數的初始化工做,並在完成初始化以後設置該變量爲真,那麼下次進入的時候便沒必要重複初始化了。api

對於問題1數組

let async1 = (cb) => {
  setTimeout(() => {
    console.log('異步任務1結束了')
    cb()
  }, 1000)
}

let async2 = (cb) => {
  setTimeout(() => {
    console.log('異步任務2結束了')
    cb()
  }, 2000)
}

let fn = () => {
  console.log('我是兩個任務都結束了才進行的任務')
}複製代碼

若是要在任務1,和任務2都結束了才進行fn任務,咱們通常的寫法是啥?
可能會下面這樣寫緩存

async1(() => {
  async2(fn)
})複製代碼

這樣確實能夠保證任務fn是在前面兩個異步任務都結束以後才進行,可是相信你是不太喜歡回調的寫法的,這裏舉的異步任務只有兩個,若是多了起來,恐怕就要蛋疼了。別疼,用下劃線的after函數能夠解救你。app

fn = _.after(2, fn)

async1(fn)
async2(fn)複製代碼

運行截圖異步

after舉例

有木有很爽,不用寫成回調地獄的形式了。那麼接下來咱們看看源碼是怎麼實現的。

after源碼實現

_.after = function(times, func) {
  return function() {
    // 只有返回的函數被調用times次以後才執行func操做
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};複製代碼

源碼簡單到要死啊,可是就是這麼神奇,妥妥地解決了咱們的問題1。

對於問題2

let app = {
  init (name, sex) {
    if (this.initialized) {
      return
    }
    // 進行參數的初始化工做
    this.name = name
    this.sex = sex
    // 初始化完成,設置標誌
    this.initialized = true
  },
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 傳參數進行應用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意這裏打印出來的是第一次傳入的參數複製代碼

通常須要且只進行一次參數初始化工做的時候,咱們可能會像上面那樣作。可是其實若是用下劃線中的before方法咱們還能夠這樣作。

let app = {
  init: _.before(2, function (name, sex) {
    // 進行參數的初始化工做
    this.name = name
    this.sex = sex
  }) ,
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// 傳參數進行應用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意這裏打印出來的是第一次傳入的參數複製代碼

好玩吧,讓咱們看看_.before是怎麼實現的。

// 建立一個函數,這個函數調用次數不超過times次
// 若是次數 >= times 則最後一次調用函數的返回值將被記住並一直返回該值

_.before = function(times, func) {
  var memo;
  return function() {
    // 返回的函數每次調用都times減1
    if (--times > 0) { 
      // 調用func,並傳入外面傳進來的參數
      // 須要注意的是,後一次調用的返回值會覆蓋前一次
      memo = func.apply(this, arguments);
    }
    // 當調用次數夠了,就將func銷燬設置爲null
    if (times <= 1) func = null;
    return memo;
  };
};複製代碼

讓函數具備記憶的功能

在程序中咱們常常會要進行一些計算的操做,當遇到比較耗時的操做時候,若是有一種機制,對於一樣的輸入,必定獲得相同的輸出,而且對於一樣的輸入,後續的計算直接從緩存中讀取,再也不須要將計算程序運行那就很是讚了。

舉例

let calculate = (num, num2) => {
  let result = 0
  let start = Date.now()
  for (let i = 0; i< 10000000; i++) { // 這裏只是模擬耗時的操做
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 這裏只是模擬耗時的操做
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}

calculate(1, 2) // 30000000
// log 獲得235
calculate(1, 2) // 30000000
// log 獲得249複製代碼

對於上面這個calculate函數,一樣的輸入1, 2,兩次調用的輸出都是同樣的,而且兩次都走了兩個耗時的循環,看看下劃線中的memoize函數,如何爲咱們省去第二次的耗時操做,直接給出300000的返回值

let calculate = _.memoize((num, num2) => {
  let start = Date.now()
  let result = 0
  for (let i = 0; i< 10000000; i++) { // 這裏只是模擬耗時的操做
    result += num
  }

  for (let i = 0; i< 10000000; i++) { // 這裏只是模擬耗時的操做
    result += num2
  }
  let end = Date.now()
  console.log(end - start)
  return result
}, function () {
  return [].join.call(arguments, '@') // 這裏是爲了給一樣的輸入指定惟一的緩存key
})

calculate(1, 2) // 30000000
// log 獲得 238
calculate(1, 2) // 30000000
// log 啥也沒有打印出,由於直接從緩存中讀取了複製代碼

源碼實現

_.memoize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    // 注意hasher,若是傳了hasher,就用hasher()執行的結果做爲緩存func()執行的結果的key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key); 
    // 若是沒有在cache中查找到對應的key就去計算一次,並緩存下來
    if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 
    // 返回結果
    return cache[address];
  };
  memoize.cache = {};
  return memoize; // 返回一個具備cache靜態屬性的函數
};複製代碼

相信你已經看懂了源碼實現,是否是很簡單,可是又很實用有趣。

來一下延時(.delay和.defer)

下劃線中在原生延遲函數setTimeout的基礎上作了一些改造,產生以上兩個函數

_.delay(function, wait, *arguments)

就是延遲wait時間去執行functionfunction須要的參數由*arguments提供

使用舉例

var log = _.bind(console.log, console)
_.delay(log, 1000, 'hello qianlongo')
// 1秒後打印出 hello qianlongo複製代碼

源碼實現

_.delay = function(func, wait) {
  // 讀取第三個參數開始的其餘參數
  var args = slice.call(arguments, 2);
  return setTimeout(function(){
    // 執行func並將參數傳入,注意apply的第一個參數是null護着undefined的時候,func內部的this指的是全局的window或者global
    return func.apply(null, args); 
  }, wait);
};複製代碼

不過有點須要注意的是_.delay(function, wait, *arguments)``function中的this指的是window或者global

_.defer(function, *arguments)

延遲調用function直到當前調用棧清空爲止,相似使用延時爲0的setTimeout方法。對於執行開銷大的計算和無阻塞UI線程的HTML渲染時候很是有用。 若是傳遞arguments參數,當函數function執行時, arguments 會做爲參數傳入

源碼實現

_.defer = _.partial(_.delay, _, 1);複製代碼

因此主要仍是看_.partial是個啥

能夠預指定參數的函數_.partial

局部應用一個函數填充在任意個數的 參數,不改變其動態this值。和bind方法很相近。你能夠在你的參數列表中傳遞_來指定一個參數 ,不該該被預先填充(underscore中文網翻譯)

使用舉例

let fn = (num1, num2, num3, num4) => {
  let str = `num1=${num1}`
  str += `num2=${num2}`
  str += `num3=${num3}`
  str += `num4=${num4}`
  return str
}

fn = _.partial(fn, 1, _, 3, _)
fn(2,4)// num1=1num2=2num3=3num4=4複製代碼

能夠看到,咱們傳入了_(這裏指的是下劃線自己)進行佔位,後續再講2和4填充到對應的位置去了。

源碼具體怎麼實現的呢?

_.partial = function(func) {
  // 獲取除了傳進回調函數以外的其餘預參數
  var boundArgs = slice.call(arguments, 1); 
  var bound = function() {
    var position = 0, length = boundArgs.length;
    // 先建立一個和boundArgs長度等長的空數組
    var args = Array(length); 
    // 處理佔位元素_
    for (var i = 0; i < length; i++) { 
      // 若是發現boundArgs中有_的佔位元素,就依次用arguments中的元素進行替換boundArgs
      args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 
    }
    // 把auguments中的其餘元素添加到boundArgs中
    while (position < arguments.length) args.push(arguments[position++]); 
    // 最後執行executeBound,接下來看看executeBound是什麼
    return executeBound(func, bound, this, this, args);
  };
  return bound;
};複製代碼

在上一篇文章如何寫一個實用的bind?
有詳細講解,這裏咱們再回顧一下
executeBound

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 若是調用方式不是new func的形式就直接調用sourceFunc,而且給到對應的參數便可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
   // 處理new調用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};複製代碼

先看一下這些參數都�表明什麼含義

  1. sourceFunc:原函數,待綁定函數
  2. boundFunc: 綁定後函數
  3. context:綁定後函數this指向的上下文
  4. callingContext:綁定後函數的執行上下文,一般就是 this
  5. args:綁定後的函數執行所需參數

這裏其實就是執行了這句,因此關鍵仍是若是處理預參數,和後續參數的邏輯

sourceFunc.apply(context, args);複製代碼

管道式函數組合

你也許遇到過這種場景,任務A,任務B,任務C必須按照順序執行,而且A的輸出做爲B的輸入,B的輸出做爲C的輸入,左後再獲得結果。用一張圖表示以下

管道

那麼通常的作法是什麼呢

let funcA = (str) => {
  return str += '-A'
}

let funcB = (str) => {
  return str += '-B'
}

let funcC = (str) => {
  return str += '-C'
}

funcC(funcB(funcA('hello')))
// "hello-A-B-C"

``` javascript 用下劃線中的`compose`方法怎麼作呢 ``` javascript
let fn = _.compose(funcC, funcB, funcA)
fn('hello')
// "hello-A-B-C"複製代碼

看起來沒有通常的作法那樣,層層繞進去了,而是以一種很是扁平的方式使用。

一樣咱們看看源碼是怎麼實現的。

_.compose源碼

_.compose = function() {
  var args = arguments;
  // 從最後一個參數開始處理
  var start = args.length - 1;
  return function() {
    var i = start;
    // 執行最後一個函數,並獲得結果result
    var result = args[start].apply(this, arguments); 
    // 從後往前一個個調用傳進來的函數,並將上一次執行的結果做爲參數傳進下一個函數
    while (i--) result = args[i].call(this, result); 
    // 最後將結果導出
    return result;
  };
};複製代碼

給多個函數綁定一樣的上下文(_.bindAll(object, *methodNames))

將多個函數methodNames綁定上下文環境爲object

😪 😪 😪,好睏,寫文章當真好要時間和精力,到這裏已經快寫了3個小時了,夜深,好像躺下睡覺啊!!!啊啊啊,再等等快說完了(但願不會誤人子弟)。

var buttonView = {
  label  : 'underscore',
  onClick: function(){ alert('clicked: ' + this.label); },
  onHover: function(){ console.log('hovering: ' + this.label); }
};
_.bindAll(buttonView, 'onClick', 'onHover');

$('#underscore_button').bind('click', buttonView.onClick);複製代碼

咱們用官網給的例子說一下,默認的jQuery中$(selector).on(eventName, callback)callback中的this指的是當前的元素自己,當時通過上面的處理,會彈出underscore

_.bindAll源碼實現

_.bindAll = function(obj) {
  var i, length = arguments.length, key;
  // 必需要指定須要綁定到obj的函數參數
  if (length <= 1) throw new Error('bindAll must be passed function names');
  // 從第一個實參開始處理,這些即是須要綁定this做用域到obj的函數
  for (i = 1; i < length; i++) { 
    key = arguments[i];
    // 調用內部的bind方法進行this綁定
    obj[key] = _.bind(obj[key], obj); 
  }
  return obj;
};複製代碼

內部使用了_.bind進行綁定,若是你對_.bind原生是如何實現的能夠看這裏如何寫一個實用的bind?

拾遺

最後關於underscore.js中function篇章還有兩個函數說一下,另外節流函數throttle以及debounce_會另外單獨寫一篇文章介紹,歡迎前往underscore-analysis/ watch一下,隨時能夠看到動態更新。

_.wrap(function, wrapper)

將第一個函數 function 封裝到函數 wrapper 裏面, 並把函數 function 做爲第一個參數傳給 wrapper. 這樣可讓 wrapper 在 function 運行以前和以後 執行代碼, 調整參數而後附有條件地執行.

直接看源碼實現吧

_.wrap = function(func, wrapper) {
    return _.partial(wrapper, func);
  };複製代碼

還記得前面說的partial吧,他會返回一個函數,內部會執行wrapper,而且func會做爲wrapper的一個參數被傳入。

_.negate(predicate)

將predicate函數執行的結果取反。

使用舉例

let fn = () => {
  return true
}

_.negate(fn)() // false複製代碼

看起來好像沒什麼軟用,可是。。。。

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(findEven) // [2, 4, 6]複製代碼

若是要找到奇數呢?

let arr = [1, 2, 3, 4, 5, 6]

let findEven = (num) => {
  return num % 2 === 0
}

arr.filter(_.negate(findEven)) // [1, 3, 5]複製代碼

源碼實現

_.negate = function(predicate) {
  return function() {
    return !predicate.apply(this, arguments);
  };
};複製代碼

源碼很簡單,就是把你傳進來的predicate函數執行的結果取反一下,可是應用仍是蠻多的。

結尾

這幾個是underscore庫中function相關的api,大部分已經說完了,若是對你有一點點幫助。

點一個小星星吧😀😀😀

點一個小星星吧😀😀😀

點一個小星星吧😀😀😀

good night 🌙

相關文章
相關標籤/搜索