你要看看這些有趣的函數(shù)方法嗎?

前言

這是underscore.js源碼分析的第六篇,如果你對(duì)這個(gè)系列感興趣,歡迎點(diǎn)擊

underscore-analysis/ watch一下,隨時(shí)可以看到動(dòng)態(tài)更新。

下劃線中有非常多很有趣的方法,可以用比較巧妙的方式解決我們?nèi)粘I钪杏龅降膯?wèn)題,比如_.after,_.before_.defer...等,也許你已經(jīng)用過(guò)他們了,今天我們來(lái)深入源碼,一探究竟,他們到底是怎么實(shí)現(xiàn)的。

function

指定調(diào)用次數(shù)(after, before)

把這兩個(gè)方法放在前面也是因?yàn)樗麄儌z能夠解決我們工作中至少以下兩個(gè)問(wèn)題

  1. 如果你要等多個(gè)異步請(qǐng)求完成之后才去執(zhí)行某個(gè)操作fn,那么你可以用_.after,而不必寫多層異步回調(diào)地獄去實(shí)現(xiàn)需求

  2. 有一些應(yīng)用可能需要進(jìn)行初始化操作而且僅需要一次初始化就可以,一般的?做法是在入口處對(duì)某個(gè)變量進(jìn)行判斷,如果為真那么認(rèn)為已經(jīng)初始化過(guò)了直接return掉,如果為假那么進(jìn)行?參數(shù)的初始化工作,并在完成初始化之后設(shè)置該變量為?真,那么?下次進(jìn)入的時(shí)候便不必重復(fù)初始化了。

對(duì)于問(wèn)題1


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

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

let fn = () => {
  console.log('我是兩個(gè)任務(wù)都結(jié)束了才進(jìn)行的任務(wù)')
}

如果要在任務(wù)1,和任務(wù)2都結(jié)束了才進(jìn)行fn任務(wù),我們一般的寫法是啥?
可能會(huì)下面這樣寫


async1(() => {
  async2(fn)
})

這樣?確實(shí)可以保證任務(wù)fn是在前面兩個(gè)異步任務(wù)都結(jié)束之后才進(jìn)行,但是相信你是不太?喜歡回調(diào)的寫法的,這里舉的異步任務(wù)只有兩個(gè),如果多了起來(lái),恐怕就要蛋疼了。別疼,用下劃線的after函數(shù)可以解救你。

fn = _.after(2, fn)

async1(fn)
async2(fn)

運(yùn)行截圖

after舉例

有木有很爽,不用寫成回調(diào)地獄的形式了。那么接下來(lái)我們看看源碼是怎么實(shí)現(xiàn)的。

after源碼實(shí)現(xiàn)

_.after = function(times, func) {
  return function() {
    // 只有返回的函數(shù)被調(diào)用times次之后才執(zhí)行func操作
    if (--times < 1) {
      return func.apply(this, arguments);
    }
  };
};

源碼?簡(jiǎn)單到要死啊,但是就是這么神奇,妥妥地解決了我們的問(wèn)題1。

對(duì)于問(wèn)題2


let app = {
  init (name, sex) {
    if (this.initialized) {
      return
    }
    // 進(jìn)行參數(shù)的初始化工作
    this.name = name
    this.sex = sex
    // 初始化完成,設(shè)置標(biāo)志
    this.initialized = true
  },
  showInfo () {
    console.log(this.name, this.sex)
  }
}

// ?傳參數(shù)進(jìn)行應(yīng)用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意?這里打印出來(lái)的是第一次傳入的參數(shù)

一般需要且只進(jìn)行一次參數(shù)初始化工作的時(shí)候,我們可能會(huì)像上面那樣做。但是其實(shí)如果用下劃線中的before方法我們還可以這樣做。

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

// ?傳參數(shù)進(jìn)行應(yīng)用的初始化

app.init('qianlonog', 'boy')
app.init('xiaohuihui', 'girl')
app.showInfo() // qianlonog boy 注意?這里打印出來(lái)的是第一次傳入的參數(shù)

好玩吧,讓我們看看_.before是怎么實(shí)現(xiàn)的。


// 創(chuàng)建一個(gè)函數(shù),這個(gè)函數(shù)調(diào)用次數(shù)不超過(guò)times次
// 如果次數(shù) >= times 則最后一次調(diào)用函數(shù)的返回值將被記住并一直返回該值

_.before = function(times, func) {
  var memo;
  return function() {
    // 返回的函數(shù)每次調(diào)用都times減1
    if (--times > 0) { 
      // 調(diào)用func,并傳入外面?zhèn)鬟M(jìn)來(lái)的參數(shù)
      // 需要注意的是,后一次調(diào)用的返回值會(huì)覆蓋前一次
      memo = func.apply(this, arguments);
    }
    // 當(dāng)調(diào)用次數(shù)夠了,就將func銷毀設(shè)置為null
    if (times <= 1) func = null;
    return memo;
  };
};

讓函數(shù)具有記憶的功能

在程序中我們經(jīng)常會(huì)要進(jìn)行一些計(jì)算的操作,當(dāng)遇到比較耗時(shí)的操作時(shí)候,如果有一種機(jī)制,對(duì)于同樣的輸入,一定得到相同的輸出,并且對(duì)于同樣的輸入,后續(xù)的計(jì)算直接從緩存中讀取,不再需要將計(jì)算程序運(yùn)行那就非常贊了。

舉例


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

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

calculate(1, 2) // 30000000
// log 得到235
calculate(1, 2) // 30000000
// log 得到249

對(duì)于上面這個(gè)calculate函數(shù),同樣的輸入1, 2,兩次調(diào)用的輸出都?是一樣的,并且兩次都走了兩個(gè)耗時(shí)的循環(huán),看看下劃線中的memoize函數(shù),如何為我們省去第二次的耗時(shí)操作,直接給出300000的返回值

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

  for (let i = 0; i< 10000000; i++) { // 這里只是模擬耗時(shí)的操作
    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 啥也沒(méi)有打印出,因?yàn)橹苯訌木彺嬷凶x取了

源碼實(shí)現(xiàn)


 _.memoize = function(func, hasher) {
  var memoize = function(key) {
    var cache = memoize.cache;
    // 注意hasher,如果傳了hasher,就用hasher()執(zhí)行的結(jié)果作為緩存func()執(zhí)行的結(jié)果的key
    var address = '' + (hasher ? hasher.apply(this, arguments) : key); 
    // 如果沒(méi)有在cache中查找到對(duì)應(yīng)的key就去計(jì)算一次,并緩存下來(lái)
    if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); 
    // 返回結(jié)果
    return cache[address];
  };
  memoize.cache = {};
  return memoize; // 返回一個(gè)具有cache靜態(tài)屬性的函數(shù)
};


相信你已經(jīng)看懂了源碼實(shí)現(xiàn),是不是很簡(jiǎn)單,但是又很實(shí)用有趣。

來(lái)一下延時(shí)(.delay和.defer)

下劃線中在原生延遲函數(shù)setTimeout的基礎(chǔ)上做了一些改造,產(chǎn)生以上兩個(gè)?函數(shù)

*_.delay(function, wait, arguments)

就是延遲wait時(shí)間去執(zhí)行functionfunction需要的參數(shù)由*arguments提供

使用舉例


var log = _.bind(console.log, console)
_.delay(log, 1000, 'hello qianlongo')
// 1秒后打印出 hello qianlongo

源碼實(shí)現(xiàn)


_.delay = function(func, wait) {
  // 讀取第三個(gè)參數(shù)開始的其他參數(shù)
  var args = slice.call(arguments, 2);
  return setTimeout(function(){
    // 執(zhí)行func并將參數(shù)傳入,注意apply的第一個(gè)參數(shù)是nul?l護(hù)著undefined的時(shí)候,func內(nèi)部的this指的是全局的window或者global
    return func.apply(null, args); 
  }, wait);
};


不過(guò)有點(diǎn)需要注意的是_.delay(function, wait, *arguments)``function中的this指的是window或者global

*_.defer(function, arguments)

延遲調(diào)用function直到當(dāng)前調(diào)用棧清空為止,類似使用延時(shí)為0的setTimeout方法。對(duì)于執(zhí)行開銷大的計(jì)算和無(wú)阻塞UI線程的HTML渲染時(shí)候非常有用。 如果傳遞arguments參數(shù),當(dāng)函數(shù)function執(zhí)行時(shí), arguments 會(huì)作為參數(shù)傳入

源碼實(shí)現(xiàn)

_.defer = _.partial(_.delay, _, 1);

所以主要還是看_.partial是個(gè)啥

可以預(yù)指定參數(shù)的函數(shù)_.partial

局部應(yīng)用一個(gè)函數(shù)填充在任意個(gè)數(shù)的 參數(shù),不改變其動(dòng)態(tài)this值。和bind方法很相近。你可以在你的參數(shù)列表中傳遞_來(lái)指定一個(gè)參數(shù) ,不應(yīng)該被預(yù)先填充(underscore中文網(wǎng)翻譯)

使用舉例


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

可以看到,我們傳入了_(這里指的是下劃線本身)進(jìn)行占位,后續(xù)再講2和4填充到對(duì)應(yīng)的位置去了。

源碼具體怎么實(shí)現(xiàn)的呢?

_.partial = function(func) {
  // 獲取除了傳進(jìn)回調(diào)函數(shù)之外的其他預(yù)參數(shù)
  var boundArgs = slice.call(arguments, 1); 
  var bound = function() {
    var position = 0, length = boundArgs.length;
    // 先創(chuàng)建一個(gè)和boundArgs長(zhǎng)度等長(zhǎng)的空數(shù)組
    var args = Array(length); 
    // 處理占位元素_
    for (var i = 0; i < length; i++) { 
      // 如果發(fā)現(xiàn)boundArgs中有_的占位元素,就依次用arguments中的元素進(jìn)行替換boundArgs
      args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; 
    }
    // 把a(bǔ)uguments中的其他元素添加到boundArgs中
    while (position < arguments.length) args.push(arguments[position++]); 
    // 最后執(zhí)行executeBound,接下來(lái)看看executeBound是什么
    return executeBound(func, bound, this, this, args);
  };
  return bound;
};

在上一篇文章如何寫一個(gè)實(shí)用的bind?
有詳細(xì)講解,這里我們?cè)倩仡櫼幌?br> executeBound

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 如果調(diào)用方式不是new func的形式就直接調(diào)用sourceFunc,并且給到對(duì)應(yīng)的參數(shù)即可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
   // 處理new調(diào)用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

先看一下這些參數(shù)都?代表什么含義

  1. sourceFunc:原函數(shù),待綁定函數(shù)
  2. boundFunc: 綁定后函數(shù)
  3. context:綁定后函數(shù)this指向的上下文
  4. callingContext:綁定后函數(shù)的執(zhí)行上下文,通常就是 this
  5. args:綁定后的函數(shù)執(zhí)行所需參數(shù)

這里?其實(shí)就是執(zhí)行了這句,所以關(guān)鍵還是如果處理預(yù)參數(shù),和后續(xù)參數(shù)的邏輯

sourceFunc.apply(context, args);

管道式函數(shù)組合

你也許遇到過(guò)這種場(chǎng)景,任務(wù)A,任務(wù)B,任務(wù)C必須按照?順序?執(zhí)行,并且A的?輸出作為B的輸入,B的輸出作為C的輸入,左后再得到結(jié)果。用一張圖表示如下

管道

那么一般的做法是什么呢

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"

看起來(lái)沒(méi)有一般的做法那樣,層層繞進(jìn)去了,而是以一種非常扁平的方式使用。

?同樣我們看看源碼是怎么實(shí)現(xiàn)的。

_.compose源碼

_.compose = function() {
  var args = arguments;
  // 從最后一個(gè)參數(shù)開始處理
  var start = args.length - 1;
  return function() {
    var i = start;
    // 執(zhí)行最后一個(gè)函數(shù),并得到結(jié)果result
    var result = args[start].apply(this, arguments); 
    // 從后往前一個(gè)個(gè)調(diào)用傳進(jìn)來(lái)的函數(shù),并將上一次執(zhí)行的結(jié)果作為參數(shù)傳進(jìn)下一個(gè)函數(shù)
    while (i--) result = args[i].call(this, result); 
    // 最后將結(jié)果導(dǎo)出
    return result;
  };
};

給多個(gè)函數(shù)綁定同樣的上下文(_.bindAll(object, *methodNames))

將多個(gè)函數(shù)methodNames綁定上下文環(huán)境為object

?? ?? ??,好困,寫文章當(dāng)真好要時(shí)間和精力,到這里已經(jīng)快寫了3個(gè)小時(shí)了,夜深,好像躺下睡覺(jué)?。。。“““?,再等等快說(shuō)完了(希望不會(huì)誤人子弟)。


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);


我們用官網(wǎng)給的例子說(shuō)一下,默認(rèn)的jQuery中$(selector).on(eventName, callback)callback中?的this指的是當(dāng)前的元素本身,當(dāng)時(shí)經(jīng)過(guò)上面的處理,會(huì)彈出underscore。

_.bindAll源碼實(shí)現(xiàn)

 _.bindAll = function(obj) {
  var i, length = arguments.length, key;
  // 必須要指定需要綁定到obj的函數(shù)參數(shù)
  if (length <= 1) throw new Error('bindAll must be passed function names');
  // 從第一個(gè)實(shí)參開始處理,這些便是需要綁定this作用域到obj的函數(shù)
  for (i = 1; i < length; i++) { 
    key = arguments[i];
    // 調(diào)用內(nèi)部的bind方法進(jìn)行this綁定
    obj[key] = _.bind(obj[key], obj); 
  }
  return obj;
};

內(nèi)部使用了_.bind進(jìn)行綁定,如果你對(duì)_.bind原生是如何實(shí)現(xiàn)的可以看這里如何寫一個(gè)實(shí)用的bind?

拾遺

最后關(guān)于underscore.js中function篇章還有兩個(gè)函數(shù)說(shuō)一下,另外節(jié)流函數(shù)throttle以及debounce_會(huì)另外?單獨(dú)?寫一篇文章介紹,歡迎前往underscore-analysis/ watch一下,隨時(shí)可以看到動(dòng)態(tài)更新。

_.wrap(function, wrapper)

將第一個(gè)函數(shù) function 封裝到函數(shù) wrapper 里面, 并把函數(shù) function 作為第一個(gè)參數(shù)傳給 wrapper. 這樣可以讓 wrapper 在 function 運(yùn)行之前和之后 執(zhí)行代碼, 調(diào)整參數(shù)然后附有條件地執(zhí)行.

直接看源碼實(shí)現(xiàn)吧

_.wrap = function(func, wrapper) {
    return _.partial(wrapper, func);
  };

還記得前面說(shuō)的partial吧,他會(huì)返回一個(gè)函數(shù),內(nèi)部會(huì)執(zhí)行wrapper,并且func?會(huì)作為wrapper的一個(gè)參數(shù)被傳入。

_.negate(predicate)

將predicate函數(shù)執(zhí)行的結(jié)果取反。

使用舉例


let fn = () => {
  return true
}

_.negate(fn)() // false

看起來(lái)好像沒(méi)什么軟用,但是。。。。


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

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

arr.filter(findEven) // [2, 4, 6]

如果要找到奇數(shù)呢?

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

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

arr.filter(_.negate(findEven)) // [1, 3, 5]

源碼實(shí)現(xiàn)


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

源碼很簡(jiǎn)單,就是把你傳進(jìn)來(lái)的predicate函數(shù)執(zhí)行的結(jié)果取反一下,但是應(yīng)用還是蠻多的。

結(jié)尾

這幾個(gè)是underscore庫(kù)中function相關(guān)的api,大部分已經(jīng)說(shuō)完了,如果對(duì)你有一點(diǎn)點(diǎn)幫助。

點(diǎn)一個(gè)小星星?吧??????

點(diǎn)一個(gè)小星星?吧??????

點(diǎn)一個(gè)小星星?吧??????

good night ??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評(píng)論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評(píng)論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評(píng)論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評(píng)論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評(píng)論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評(píng)論 0 291
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評(píng)論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評(píng)論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評(píng)論 1 296
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容