02.28 高階函數,你怎麼那麼漂亮呢

點擊上方 "程序員小樂"關注, 星標或置頂一起成長

每天凌晨00點00分, 第一時間與你相約


每日英文

Sometimes, it is better to be alone. That way, nobody can hurt you.

有時候,孤身一人反而更好。那樣,就沒有人能傷害你了。


每日掏心話

人生匆匆,若不時而駐足欣賞,將會錯過它的美好。感情裡,最缺的不是激情,而是激情過後的那份堅持。


來自:chenhongdong | 責編:樂樂

鏈接:juejin.im/post/5ad6b34a6fb9a028cc61bfb3

高階函數,你怎麼那麼漂亮呢

程序員小樂(ID:study_tech)第 790 次推文 圖片來自百度


往日回顧:又一個程序員“刪庫跑路”被抓了,一己之力蒸發公司市值超10億,300萬商鋪遭癱瘓


正文


不廢話,先來看下什麼是高階函數

高階函數


  • 函數可以作為參數傳遞

  • 函數可以作為返回值輸出


函數作為參數傳遞


  • 回調函數

  • 在ajax異步請求的過程中,回調函數使用的非常頻繁

  • 在不確定請求返回的時間時,將callback回調函數當成參數傳入

  • 待請求完成後執行callback函數


下面看個簡單的demo:

說實在的本來只是個簡單的,不過越寫越興奮,就弄成了個小demo了,大家也可以copy下去自己添油加醋一下(寫成各種版本),樂呵一下吧,PS:由於代碼過多佔用文章,將css樣式去掉了,樣式的實現大家隨意發揮就好了


  • html結構



js部分


  • // index.js

  • // 回調函數

  • // 異步請求

  • let getInfo = function (keywords, callback) {

  • $.ajax({

  • url: 'http://musicapi.leanapp.cn/search', // 以網易雲音樂為例

  • data: {

  • keywords

  • },

  • success: function (res) {

  • callback && callback(res.result.songs);

  • }

  • })

  • };


  • $('#btn').on('click', function() {

  • let keywords = $(this).prev().val();

  • $('#loading').show();

  • getInfo(keywords, getData);

  • });

  • // 加入回車

  • $("#search_inp").on('keyup', function(e){

  • if (e.keyCode === 13) {

  • $('#loading').show();

  • getInfo(this.value, getData);

  • }

  • });


  • function getData(data) {

  • if (data && data.length) {

  • let html = render(data);

  • // 初始化Dom結構

  • initDom(html, function(wrap) {

  • play(wrap);

  • });

  • }

  • }

  • // 格式化時間戳

  • function formatDuration(duration) {

  • duration = parseInt(duration / 1000); // 轉換成秒

  • let hour = Math.floor(duration / 60 / 60),

  • min = Math.floor((duration % 3600) / 60),

  • sec = duration % 60,

  • result = '';


  • result += `${fillIn(min)}:${fillIn(sec)}`;

  • return result;

  • }


  • function fillIn(n) {

  • return n < 10 ? '0' + n : '' + n;

  • }


  • let initDom = function (tmp, callback) {

  • $('.item').remove();

  • $('#loading').hide();

  • $('#box').append(tmp);

  • // 這裡因為不知道dom合適才會被完全插入到頁面中

  • // 所以用callback當參數,等dom插入後再執行callback

  • callback && callback(box);

  • };


  • let render = function (data) {

  • let template = '';

  • let set = new Set(data);

  • data = [...set]; // 可以利用Set去做下簡單的去重,可忽略這步

  • for (let i = 0; i < 8; i++) {

  • let item = data[i];

  • let name = item.name;

  • let singer = item.artists[0].name;

  • let pic = item.album.picUrl;

  • let time = formatDuration(item.duration);


  • template += `

  • ${name}

  • ${singer}

  • <audio>

  • `;
  • }

  • return template;

  • };


  • let play = function(wrap) {

  • wrap = $(wrap);

  • wrap.on('click', '.item', function() {

  • let self = $(this),

  • $audio = self.find('audio'),

  • $allAudio = wrap.find('audio');


  • for (let i = 0; i < $allAudio.length; i++) {

  • $allAudio[i].pause();

  • }

  • $audio[0].play();

  • self.addClass('play').siblings('.item').removeClass('play');

  • });

  • };


  • 按照上面的代碼啪啪啪,就會得到下面這樣的效果,一起來看下吧

    高階函數,你怎麼那麼漂亮呢

    不過依然感謝網易雲音樂提供的API接口,讓我們聆聽美妙好音樂


    • 好了迴歸主旋律,前面的戲份有點過了,不知不覺居然寫了個小demo,確實有點過分了

    • 本來是說一下函數作為參數傳遞的應用,寫的太多了,趕緊調轉船頭繼續講吧


    函數作為返回值輸出

    親們,函數作為返回值輸出的應用場景那就太多了,這也體現了函數式編程的思想。其實從閉包的例子中我們就已經看到了關於高階函數的相關內容了,哈哈

    還記得在我們去判斷數據類型的時候,我們都是通過Object.prototype.toString來計算的。每個數據類型之間只是'[object XXX]'不一樣罷了

    所以在我們寫類型判斷的時候,一般都是將參數傳入函數中,這裡我簡單寫一下實現,咱們先來看看


    • function isType(type) {

    • return function(obj) {

    • return Object.prototype.toString.call(obj) === `[object ${type}]

    • }

    • }


    • const isArray = isType('Array');

    • const isString = isType('String');

    • console.log(isArray([1, 2, [3,4]]); // true

    • console.log(isString({}); // false


    其實上面實現的isType函數,也屬於偏函數的範疇,偏函數實際上是返回了一個包含預處理參數的新函數,以便之後可以調用

    另外還有一種叫做預置函數,它的實現原理也很簡單,當達到條件時再執行回調函數

    function after(time, cb) { return function() { if (--time === 0) { cb(); } }}// 舉個栗子吧,吃飯的時候,我很能吃,吃了三碗才能吃飽let eat = after(3, function() { console.log('吃飽了');});eat();eat();eat();

    上面的eat函數只有執行3次的時候才會輸出'吃飽了',還是比較形象的。

    這種預置函數也是js中巧妙的裝飾者模式的實現,裝飾者模式在實際開發中也非常有用,再以後的歲月裡我也會好好研究之後分享給大家的

    好了,不要停,不要停,再來看一個栗子


    • // 這裡我們創建了一個單例模式

    • let single = function (fn) {

    • let ret;

    • return function () {

    • console.log(ret); // render一次undefined,render二次true,render三次true

    • // 所以之後每次都執行ret,就不會再次綁定了

    • return ret || (ret = fn.apply(this, arguments));

    • }

    • };


    • let bindEvent = single(function () {

    • // 雖然下面的renders函數執行3次,bindEvent也執行了3次

    • // 但是根據單例模式的特點,函數在被第一次調用後,之後就不再調用了

    • document.getElementById('box').onclick = function () {

    • alert('click');

    • }

    • return true;

    • });


    • let renders = function () {

    • console.log('渲染');

    • bindEvent();

    • }


    • renders();

    • renders();

    • renders();


    這個高階函數的栗子,可以說一石二鳥啊,既把函數當做參數傳遞了,又把函數當返回值輸出了。

    單例模式也是一種非常實用的設計模式,在以後的文章中也會針對這些設計模式去分析的,敬請期待,哈哈,下面再看看高階函數還有哪些用途

    其他應用

    函數柯里化

    柯里化又稱部分求值,柯里化函數會接收一些參數,然後不會立即求值,而是繼續返回一個新函數,將傳入的參數通過閉包的形式保存,等到被真正求值的時候,再一次性把所有傳入的參數進行求值

    還能闡述的更簡單嗎?在一個函數中填充幾個參數,然後再返回一個新函數,最後進行求值,沒了,是不是說的簡單了

    說的再簡單都不如幾行代碼演示的清楚明白


    • // 普通函數

    • function add(x,y){

    • return x + y;

    • }


    • add(3,4); // 7


    • // 實現了柯里化的函數

    • // 接收參數,返回新函數,把參數傳給新函數使用,最後求值

    • let add = function(x){

    • return function(y){

    • return x + y;

    • }

    • };


    • add(3)(4); // 7


    以上代碼非常簡單,只是起個引導的作用。下面我們來寫一個通用的柯里化函數


    • function curry(fn) {

    • let slice = Array.prototype.slice, // 將slice緩存起來

    • args = slice.call(arguments, 1); // 這裡將arguments轉成數組並保存


    • return function() {

    • // 將新舊的參數拼接起來

    • let newArgs = args.concat(slice.call(arguments));

    • return fn.apply(null, newArgs); // 返回執行的fn並傳遞最新的參數

    • }

    • }


    實現了通用的柯里化函數,了不起啊,各位很了不起啊。

    不過這還不夠,我們還可以利用ES6再來實現一下,請看如下代碼:


    • // ES6版的柯里化函數

    • function curry(fn) {

    • const g = (...allArgs) => allArgs.length >= fn.length ?

    • fn(...allArgs) :

    • (...args) => g(...allArgs, ...args)


    • return g;

    • }


    • // 測試用例

    • const foo = curry((a, b, c, d) => {

    • console.log(a, b, c, d);

    • });

    • foo(1)(2)(3)(4); // 1 2 3 4

    • const f = foo(1)(2)(3);

    • f(5); // 1 2 3 5


    兩種不同的實現思路相同,之後可以試著分析一下

    不過大家有沒有發現我們在ES5中使用的bind方法,其實也利用了柯里化的思想,那麼再來看一下下


    • let obj = {

    • songs: '以父之名'

    • };


    • function fn() {

    • console.log(this.songs);

    • }


    • let songs = fn.bind(obj);

    • songs(); // '以父之名'


    為什麼這麼說?這也看不出什麼頭緒啊,別捉急,再來看一下bind的實現原理


    • Function.prototype.bind = function(context) {

    • let self = this,

    • slice = Array.prototype.slice,

    • args = slice.call(arguments);


    • return function() {

    • return self.apply(context, args.slice(1));

    • }

    • };


    是不是似曾相識,是不是,是不是,有種師出同門的趕腳了啊

    反柯里化

    啥?反柯里化,剛剛被柯里化弄的手舞足蹈的,現在又出現了個反柯里化,有木有搞錯啊!那麼反柯里化是什麼呢?簡而言之就是函數的借用,天下函數(方法)大家用

    比如,一個對象未必只能使用它自身的方法,也可以去借用原本不屬於它的方法,要實現這點似乎就很簡單了,因為call和apply就可以完成這個任務


    • (function() {

    • // arguments就借用了數組的push方法

    • let result = Array.prototype.slice.call(arguments);

    • console.log(result); // [1, 2, 3, 'hi']

    • })(1, 2, 3, 'hi');


    • Math.max.apply(null, [1,5,10]); // 數組借用了Math.max方法


    從以上代碼中看出來了,大家都是相親相愛的一家人。利用call和apply改變了this指向,方法中用到的this再也不侷限在原來指定的對象上了,加以泛化後得到更廣的適用性

    反柯里化的話題是由我們親愛的js之父發表的,我們來從實際例子中去看一下它的作用


    • let slice = Array.prototype.slice.uncurrying();


    • (function() {

    • let result = slice(arguments); // 這裡只需要調用slice函數即可

    • console.log(result); // [1, 2, 3]

    • })(1,2,3);


    以上代碼通過反柯里化的方式,把Array.prototype.slice變成了一個通用的slice函數,這樣就不會侷限於僅對數組進行操作了,也從而將函數調用顯得更為簡潔清晰了

    最後再來看一下它的實現方式吧,看代碼,更逼真


    • Function.prototype.uncurrying = function() {

    • let self = this; // self 此時就是下面的Array.prototype.push方法

    • return function() {

    • let obj = Array.prototype.shift.call(arguments);

    • /*

    • obj其實是這種樣子的

    • obj = {

    • 'length': 1,

    • '0': 1

    • }

    • */

    • return self.apply(obj, arguments); // 相當於Array.prototype.push(obj, 110)

    • }

    • };

    • let slice = Array.prototype.push.uncurrying();


    • let obj = {

    • 'length': 1,

    • '0': 1

    • };

    • push(obj, 110);

    • console.log(obj); // { '0': 1, '1': 110, length: 2 }


    其實實現反柯里化的方式不只一種,下面再給大家分享一種,直接看代碼

    Function.prototype.uncurrying = function() { let self = this; return function() { return Function.prototype.call.apply(self, arguments); }};

    實現方式大致相同,大家也可以寫一下試試,動動手,活動一下筋骨

    函數節流

    下面再說一下函數節流,我們都知道在onresize、onscroll和mousemove,上傳文件這樣的場景下,函數會被頻繁的觸發,這樣很消耗性能,瀏覽器也會吃不消的

    於是大家開始研究一種高級的方法,那就是控制函數被觸發的頻率,也就是函數節流了。簡單說一下原理,利用setTimeout在一定的時間內,函數只觸發一次,這樣大大降低了頻率問題

    函數節流的實現也多種多樣,這裡我們實現大家常用的吧


    • function throttle (fn, wait) {

    • let _fn = fn, // 保存需要被延遲的函數引用

    • timer,

    • flags = true; // 是否首次調用


    • return function() {

    • let args = arguments,

    • self = this;


    • if (flags) { // 如果是第一次調用不用延遲,直接執行即可

    • _fn.apply(self, args);

    • flags = false;

    • return flags;

    • }

    • // 如果定時器還在,說明上一次還沒執行完,不往下執行

    • if (timer) return false;


    • timer = setTimeout(function() { // 延遲執行

    • clearTimeout(timer); // 清空上次的定時器

    • timer = null; // 銷燬變量

    • _fn.apply(self, args);

    • }, wait);

    • }

    • }


    • window.onscroll = throttle(function() {

    • console.log('滾動');

    • }, 500);


    給頁面上body設置一個高度出現滾動條後試試看,比每滾動一下就觸發來說,大大降低了性能的損耗,這就是函數節流的作用,起到了事半功倍的效果,開發中也比較常用的

    分時函數

    我們知道有一個典故叫做:羅馬不是一天建成的;更為通俗的來說,胖紙也不是一天吃成的

    體現在程序裡也是一樣,我們如果一次獲得了很多數據(比如有10W數據),然後在前端渲染的時候會卡到爆,瀏覽器那麼溫柔的物種都會起來罵娘了

    所以在處理這麼多數據的時候,我們可以選擇分批進行,不用一次塞辣麼多,嘴就辣麼大

    下面來看一下簡單的實現


    • function timeChunk(data, fn, count = 1, wait) {

    • let obj, timer;


    • function start() {

    • let len = Math.min(count, data.length);

    • for (let i = 0; i < len; i++) {

    • val = data.shift(); // 每次取出一個數據,傳給fn當做值來用

    • fn(val);

    • }

    • }


    • return function() {

    • timer = setInterval(function() {

    • if (data.length === 0) { // 如果數據為空了,就清空定時器

    • return clearInterval(timer);

    • }

    • start();

    • }, wait); // 分批執行的時間間隔

    • }

    • }


    • // 測試用例

    • let arr = [];

    • for (let i = 0; i < 100000; i++) { // 這裡跑了10萬數據

    • arr.push(i);

    • }

    • let render = timeChunk(arr, function(n) { // n為data.shift()取到的數據

    • let div = document.createElement('div');

    • div.innerHTML = n;

    • document.body.appendChild(div);

    • }, 8, 20);


    • render();


    惰性加載

    兼容現代瀏覽器以及IE瀏覽器的事件添加方法就是一個很好的栗子

    // 常規的是這樣寫的let addEvent = function(ele, type, fn) { if (window.addEventListener) { return ele.addEventListener(type, fn, false); } else if (window.attachEvent) { return ele.attachEvent('on' + type, function() { fn.call(ele); }); }};

    這樣實現有一個缺點,就是在調用addEvent的時候都會執行分支條件裡,其實只需要判斷一次就行了,非要每次執行都來一波

    下面我們再來優化一下addEvent,以規避上面的缺點,就是我們要實現的惰性加載函數了


    • let addEvent = function(ele, type, fn) {

    • if (window.addEventListener) {

    • addEvent = function(ele, type, fn) {

    • ele.addEventListener(type, fn, false);

    • }

    • } else if (window.attachEvent) {

    • addEvent = function(ele, type, fn) {

    • ele.attachEvent('on' + type, function() {

    • fn.call(ele)

    • });

    • }

    • }


    • addEvent(ele, type, fn);

    • };


    上面的addEvent函數還是個普通函數,還是有分支判斷。不過當第一次進入分支條件後,在內部就會重寫了addEvent函數

    下次再進入addEvent函數的時候,函數里就不存在條件判斷了

    終點

    節目不早,時間剛好,又到了該要說再見的時候了,來一個結束語吧

    高階函數


    • 可以把函數當做參數傳遞和返回值輸出

    • 函數柯里化

    • 接收參數,返回新函數,把參數傳給新函數,最後求值

    • 定義

    • 作用

    • 參數複用 (add函數栗子)

    • 提前返回 (惰性加載)

    • 延遲計算 (bind)

    • 反柯里化

    • 統一方法,讓天下沒有不能用的方法

    • 函數節流

    • 將頻繁調用的函數設定在一個時間內執行,防止多次觸發

    • 分時函數

    • 一次性加載太多太多數據,吃不消,可以像node中流一樣,慢慢來,別急

    • 惰性加載

    • 函數執行的分支僅會發生一次


    高階函數,你怎麼那麼漂亮呢

    歡迎在留言區留下你的觀點,一起討論提高。如果今天的文章讓你有新的啟發,學習能力的提升上有新的認識,歡迎轉發分享給更多人。


    猜你還想看


    阿里、騰訊、百度、華為、京東最新面試題彙集

    微服務的生離死別,看這篇就對了!

    Java中的對象都是在堆上分配的嗎?

    IntelliJ IDEA 最常用配置詳細圖解,適合剛剛用的新人!

    關注訂閱號「程序員小樂」,收看更多精彩內容
    嘿,你在看嗎?


    分享到:


    相關文章: