概述 理解柯里化函數,需要有閉包的基礎,只有徹底理解閉包後才能理解柯里化,如果尚未理解閉包,建議閱讀上文js引擎的執行過程(一);如果理解了閉包再研究柯里化函數,則會大大的加深你對閉包理解,並且更清楚的認識到閉包的應用場景,那麼如果在面試時候問到閉包,你就可以侃侃而談了;並且理解柯里化函數會在很大的 ...
概述
理解柯里化函數,需要有閉包的基礎,只有徹底理解閉包後才能理解柯里化,如果尚未理解閉包,建議閱讀上文js引擎的執行過程(一);如果理解了閉包再研究柯里化函數,則會大大的加深你對閉包理解,並且更清楚的認識到閉包的應用場景,那麼如果在面試時候問到閉包,你就可以侃侃而談了;並且理解柯里化函數會在很大的程度上提升函數式編程的能力,輕鬆解決各種複雜的編程問題。
說了這麼多柯里化的好處,接下來我們趕緊學習柯里化吧!
在維基百科和百度百科中,對柯里化的定義是這樣的,如下:
柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。
由以上定義,柯里化又可理解為部分求值,返回接受剩餘參數且返回結果的新函數。想要應用柯里化,我們必須先理解柯里化的作用和特點,這裡我總結為以下三點:
-
參數復用 – 復用最初函數的第一個參數
-
提前返回 – 返回接受餘下的參數且返回結果的新函數
-
延遲執行 – 返回新函數,等待執行
瞭解柯里化的作用特點後,我們可以簡單理解為當一個函數需要提前處理並需要等待執行或者接受多個不同作用的參數時候,我們便可應用柯里化。單純講解概念難免抽象,接下來,我們具體分析柯里化函數的應用。
應用
如果為了分析柯里化函數而編造一些簡單例子去分析,那麼難免會體現不出來柯里化的作用,而且也會僅限於理解柯里化而不知道應該在什麼場景下應用柯里化函數,所以這裡直接用我們在編程中經常接觸的例子進行柯里化函數封裝,並以此來理解柯里化函數。在編程開發中,使用柯里化函數封裝解決問題的例子主要有:
-
相容瀏覽器事件監聽方法
-
性能優化:防抖和節流
-
相容低版本IE的bind方法
本文主要對上面三個場景案例進行詳細分析,案例難度大小為從上至下,全面介紹柯里化函數,希望能幫助大家理解柯里化。
事件監聽
原生事件監聽的方法在現代瀏覽器和IE瀏覽器會有相容問題,解決該相容性問題的方法是進行一層封裝,若不考慮柯里化函數,我們正常情況下會像下麵這樣進行封裝,如下:
/*
|
該封裝方法完全沒有問題,但是有個唯一的缺陷就是,當我們每次調用addEvent
方法時,都會執行一次if...else if...
,進行一次相容判斷。其實每次都執行相容判斷是完全沒有必要的,那有沒有辦法只做一次判斷呢?這個時候,柯里化函數就派上用場了,如下:
var addEvent = (function() {
|
這個例子利用了柯里化提前返回和延遲執行的特點,如下:
-
提前返回 – 使用函數立即調用進行了一次相容判斷(部分求值),返回相容的事件綁定方法
-
延遲執行 – 返回新函數,在新函數調用相容的事件方法。等待
addEvent
新函數調用,延遲執行
這就是柯里化函數的基本用法 – 提前返回和延遲執行,但是這裡沒有利用到柯里化參數復用的特點,接下來我們繼續分析防抖和節流。
防抖和節流
在web開發中,頁面高頻率觸發的事件非常多,例如scroll,resize,mousemove等等,但是瀏覽器頁面渲染的幀頻為60fps,意思就是每秒刷新60幀,每1000/60約等於16.7ms刷新一次幀。
我們試想一下,如果高頻事件的觸發頻率過快,以大於或者遠大於16.7ms/幀的頻率觸發,會出現什麼問題?
事件觸發頻率大於瀏覽器的顯示頻率(16.7ms/幀),即瀏覽器顯示跟不上事件觸發的頻率,若事件處理函數中涉及DOM操作,則會導致瀏覽器掉幀,繼而導致動畫斷續顯示,畫面粘滯,在很大程度上影響用戶體驗。
如果在高頻事件以大於16.7ms/幀的速度進行,並且在該事件處理函數中進行大量的計算或DOM操作,會出現什麼問題?
由於該事件處理函數複雜且觸發過於頻繁,會導致上一次事件觸發的操作計算無法在下一次事件觸發前完成,則會使瀏覽器CPU使用率不斷增加,繼而造成瀏覽器卡頓甚至崩潰,如下圖所示:
註:註意觀察圖片左邊的控制台輸出以及右邊瀏覽器任務管理器的CPU使用率。
由上面的問題,我們可以知道,使用高頻事件時必須先解決其潛在的問題,才能保證頁面性能。針對以上問題,我們可用以下兩點方法從根本上上解決問題,如下:
-
高頻事件處理函數,不應該含有複雜的操作,例如DOM操作和複雜計算(DOM操作一般會造成頁面迴流和重繪,使瀏覽器不斷重新渲染頁面,若有疑問可閱讀上文 – 瀏覽器渲染過程)。
-
控制高頻事件的觸發頻率
控制高頻事件的觸發頻率是關鍵點,但是事件觸發是原生事件監聽方法進行監聽的,那麼我們該如何控制?
如果我們能延遲事件處理函數的執行,那麼就相當於控制了事件的觸發頻率,然後再通過保存執行狀態來控制事件處理函數的執行,那麼整個問題就可以迎刃而解了。
其中防抖和節流對高頻事件進行優化的原理就是通過延遲執行,將多個間隔接近的函數執行合併成一次函數執行。下麵將會詳細講解。
防抖(Debouncing)
針對高頻事件,防抖就是將多個觸發間隔接近的事件函數執行,合併成一次函數執行。
實現防抖的關鍵點主要有兩個,如下:
-
使用setTimeout延時器,傳入的延遲時間,將事件處理函數延遲執行,並且通過事件觸發頻率與延遲時間值的比較,控制處理函數是否執行
-
使用柯里化函數結合閉包的思想,將執行狀態保存在閉包中,返回新函數,在新函數中通過執行狀態控制是否在滾動時執行處理函數
實現代碼如下:
/*
|
防抖技術使用如下:
var debounceScroll = debounce(function() {
|
防抖技術僅靠傳入延遲時間值的大小控制高頻事件的觸發頻率,如果傳入的延遲時間值比較大,那麼就會出現一定的問題。例如當傳入延遲時間為1000ms,那麼當用戶滾動速度大於1000ms/次時,則無論滑鼠滾動多久都不會觸發事件處理函數。因此防抖技術存在一定的缺陷,會不適用於某些場景,例如圖片懶載入。這個時候節流就派上用場了。
節流(Throttle)
節流也是將多個觸發間隔接近的事件函數執行,合併成一次函數執行,並且在指定的時間內至少執行一次事件處理函數。
節流實現原理跟防抖技術類似,但是比防抖多了一次函數執行判斷,實現的關鍵點是:
- 利用閉包存儲了當前和上一次執行的時間戳,通過兩次函數執行的時間差跟指定的延遲時間的比較,控制函數是否立刻執行
實現代碼如下:
/*
|
註:以上防抖和節流函數封裝是根據個人理解進行封裝的,若想對比不同的封裝方法,建議閱讀第三方underscore函數庫中的throttle和debounce的實現方法,原理大致是一樣的,但是封裝思維稍有不同。
拓展:
以上的節流和防抖技術都是用setTimeout實現的,是否有其他的實現方案,性能是否會更好?
可直接使用瀏覽器幀頻刷新自動調用的方法(requestAnimationFrame)實現,實現起來會更加簡單,而且性能會更好,但是唯一缺點就是需要自行解決低版本的IE瀏覽器相容問題,實現代碼如下:
//解決requestAnimationFrame相容問題
|
bind函數柯里化
函數的bind方法相信我們都不陌生,但是低版本的IE瀏覽器不相容bind方法,想要繼續在低版本的IE瀏覽器中使用bind方法,則需要我們自行封裝bind方法,實現的關鍵點是:
- bind方法改變this指向,卻不會執行原函數,那麼我們可利用柯里化延遲執行,參數復用和提前返回的特點,返回新函數,在新函數使用apply方法執行原函數
我們這裡將bind方法封裝分為兩種情況,如下:
-
第一種:簡單的bind方法封裝(不考慮構造函數,僅用於普通函數),實現代碼如下:
if (!Function.prototype.bind) {
Function.prototype.bind = function(context) {
if(context.toString() !== "[object Object]" && context.toString() !== "[object Window]" ) {
throw TypeError("context is not a Object.")
}
var _this = this;
var args = [].slice.call(arguments, 1);
return function() {
var _args = [].slice.call(arguments);
_this.apply(context, _args.concat(args))
}
}
} -
第二種:複雜情況(考慮bind的任何用法),這裡直接使用MDN的bind相容方法,如下:
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 獲取調用時(fBound)的傳參.bind 返回的函數入參往往是這麼傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關係
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
};
要理解複雜的bind相容方法,必須徹底理解以下四個基礎知識:
-
js的原型對象
-
構造函數使用new操作符的過程
-
this的指向問題
-
熟悉bind方法的使用場景
圍繞以上四個關鍵點思考,bind的封裝思想便可理解,這裡不做過多解釋。
柯里化函數封裝
分析了柯里化的各種使用場景,相信我們已經大概感受到柯里化的好處了 – 部分求值,將複雜問題分步求解,變得更簡單化。這裡我們可以嘗試封裝一個簡單的柯里化函數,如下:
function createCurry(fn) {
|
柯里化函數的特點如上註釋所示:
-
復用第一個參數
-
返回新函數
-
收集剩餘參數
-
返回結果
柯里化函數的簡單例子應用,如下:
//add(19)(10, 20, 30),求該函數傳遞的參數和
|
總結
以上便是柯里化函數的基本應用以及原理,希望可以提升大家對柯里化函數以及函數式編程的理解,如有錯誤,敬請指正。
[鏈接](https://heyingye.github.io/2018/04/20/%E6%9F%AF%E9%87%8C%E5%8C%96%E5%87%BD%E6%95%B0%E5%BA%94%E7%94%A8/)