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

謙龍發表於2017-05-25

前言

這是underscore.js原始碼分析的第六篇,如果你對這個系列感興趣,歡迎點選

underscore-analysis/ watch一下,隨時可以看到動態更新。

下劃線中有非常多很有趣的方法,可以用比較巧妙的方式解決我們日常生活中遇到的問題,比如_.after,_.before_.defer...等,也許你已經用過他們了,今天我們來深入原始碼,一探究竟,他們到底是怎麼實現的。

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

指定呼叫次數(after, before)

把這兩個方法放在前面也是因為他們倆能夠解決我們工作中至少以下兩個問題

  1. 如果你要等多個非同步請求完成之後才去執行某個操作fn,那麼你可以用_.after,而不必寫多層非同步回撥地獄去實現需求

  2. 有一些應用可能需要進行初始化操作而且僅需要一次初始化就可以,一般的做法是在入口處對某個變數進行判斷,如果為真那麼認為已經初始化過了直接return掉,如果為假那麼進行引數的初始化工作,並在完成初始化之後設定該變數為真,那麼下次進入的時候便不必重複初始化了。

對於問題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函式可以解救你。

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 ?

相關文章