高階函式,你怎麼那麼漂亮呢!

chenhongdong發表於2018-04-18

Nice to meet you!

能來到這裡就代表已經看過上一篇關於並不孤獨的閉包文章了(沒看不能來?當然......可以來),那麼就不廢話了,先來看下什麼是高階函式

高階函式

  • 函式可以作為引數傳遞
  • 函式可以作為返回值輸出

函式作為引數傳遞

  • 回撥函式
    • 在ajax非同步請求的過程中,回撥函式使用的非常頻繁
    • 在不確定請求返回的時間時,將callback回撥函式當成引數傳入
    • 待請求完成後執行callback函式

下面看個簡單的demo

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

  • html結構
<body>
    <div id="box" class="clearfix"></div>
    <script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script src="./index.js"></script>
</body>
複製程式碼

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 += `
            <div class="item">
                <div class="pic" data-time="${time}">
                    <span></span>
                    <img src="${pic}" />
                </div>
                <h4>${name}</h4>
                <p>${singer}</p>
                <audio src="http://music.163.com/song/media/outer/url?id=${item.id}.mp3"></audio>
            </div>`;
    }
    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函式的時候,函式裡就不存在條件判斷了

終點

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

  • 高階函式
    • 可以把函式當做引數傳遞和返回值輸出
    • 函式柯里化
      • 定義
        • 接收引數,返回新函式,把引數傳給新函式,最後求值
      • 作用
        1. 引數複用 (add函式栗子)
        2. 提前返回 (惰性載入)
        3. 延遲計算 (bind)
    • 反柯里化
      • 統一方法,讓天下沒有不能用的方法
    • 函式節流
      • 將頻繁呼叫的函式設定在一個時間內執行,防止多次觸發
    • 分時函式
      • 一次性載入太多太多資料,吃不消,可以像node中流一樣,慢慢來,別急
    • 惰性載入
      • 函式執行的分支僅會發生一次

我勒個去,居然羅列了這麼多東西,大家看的也很辛苦了,早睡早起,好好休息吧!

這兩篇也是為了給觀察者模式起個頭,之後會繼續寫文章來和大家好好分享的,謝謝各位小主,阿哥的觀看了!哈哈

相關文章