(轉載)柯里化函數應用

来源:https://www.cnblogs.com/duiniweixiao/archive/2018/04/23/8919695.html
-Advertisement-
Play Games

概述 理解柯里化函數,需要有閉包的基礎,只有徹底理解閉包後才能理解柯里化,如果尚未理解閉包,建議閱讀上文js引擎的執行過程(一);如果理解了閉包再研究柯里化函數,則會大大的加深你對閉包理解,並且更清楚的認識到閉包的應用場景,那麼如果在面試時候問到閉包,你就可以侃侃而談了;並且理解柯里化函數會在很大的 ...


概述

理解柯里化函數,需要有閉包的基礎,只有徹底理解閉包後才能理解柯里化,如果尚未理解閉包,建議閱讀上文js引擎的執行過程(一);如果理解了閉包再研究柯里化函數,則會大大的加深你對閉包理解,並且更清楚的認識到閉包的應用場景,那麼如果在面試時候問到閉包,你就可以侃侃而談了;並且理解柯里化函數會在很大的程度上提升函數式編程的能力,輕鬆解決各種複雜的編程問題。

說了這麼多柯里化的好處,接下來我們趕緊學習柯里化吧!

在維基百科和百度百科中,對柯里化的定義是這樣的,如下:
柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數而且返回結果的新函數的技術。

由以上定義,柯里化又可理解為部分求值,返回接受剩餘參數且返回結果的新函數。想要應用柯里化,我們必須先理解柯里化的作用和特點,這裡我總結為以下三點:

  • 參數復用 – 復用最初函數的第一個參數

  • 提前返回 – 返回接受餘下的參數且返回結果的新函數

  • 延遲執行 – 返回新函數,等待執行

瞭解柯里化的作用特點後,我們可以簡單理解為當一個函數需要提前處理並需要等待執行或者接受多個不同作用的參數時候,我們便可應用柯里化。單純講解概念難免抽象,接下來,我們具體分析柯里化函數的應用。

 

應用

如果為了分析柯里化函數而編造一些簡單例子去分析,那麼難免會體現不出來柯里化的作用,而且也會僅限於理解柯里化而不知道應該在什麼場景下應用柯里化函數,所以這裡直接用我們在編程中經常接觸的例子進行柯里化函數封裝,並以此來理解柯里化函數。在編程開發中,使用柯里化函數封裝解決問題的例子主要有:

  • 相容瀏覽器事件監聽方法

  • 性能優化:防抖和節流

  • 相容低版本IE的bind方法

本文主要對上面三個場景案例進行詳細分析,案例難度大小為從上至下,全面介紹柯里化函數,希望能幫助大家理解柯里化。

 

事件監聽

原生事件監聽的方法在現代瀏覽器和IE瀏覽器會有相容問題,解決該相容性問題的方法是進行一層封裝,若不考慮柯里化函數,我們正常情況下會像下麵這樣進行封裝,如下:

/*
* @param ele Object DOM元素對象
* @param type String 事件類型
* @param fn Function 事件處理函數
* @param isCapture Boolean 是否捕獲
*/
var addEvent = function(ele, type, fn, isCapture) {
if(window.addEventListener) {

ele.addEventListener(type, fn, isCapture)

} else if(window.attachEvent) {

ele.attachEvent("on" + type, fn)
}
}

 

該封裝方法完全沒有問題,但是有個唯一的缺陷就是,當我們每次調用addEvent方法時,都會執行一次if...else if...,進行一次相容判斷。其實每次都執行相容判斷是完全沒有必要的,那有沒有辦法只做一次判斷呢?這個時候,柯里化函數就派上用場了,如下:

var addEvent = (function() {
if(window.addEventListener) {
return function(ele, type, fn, isCapture) {
ele.addEventListener(type, fn, isCapture)
}
} else if(window.attachEvent) {
return function(ele, type, fn) {
ele.attachEvent("on" + type, fn)
}
}
})()

 

這個例子利用了柯里化提前返回延遲執行的特點,如下:

  • 提前返回 – 使用函數立即調用進行了一次相容判斷(部分求值),返回相容的事件綁定方法

  • 延遲執行 – 返回新函數,在新函數調用相容的事件方法。等待addEvent新函數調用,延遲執行

這就是柯里化函數的基本用法 – 提前返回延遲執行,但是這裡沒有利用到柯里化參數復用的特點,接下來我們繼續分析防抖和節流。

 

防抖和節流

在web開發中,頁面高頻率觸發的事件非常多,例如scroll,resize,mousemove等等,但是瀏覽器頁面渲染的幀頻為60fps,意思就是每秒刷新60幀,每1000/60約等於16.7ms刷新一次幀。

我們試想一下,如果高頻事件的觸發頻率過快,以大於或者遠大於16.7ms/幀的頻率觸發,會出現什麼問題?

事件觸發頻率大於瀏覽器的顯示頻率(16.7ms/幀),即瀏覽器顯示跟不上事件觸發的頻率,若事件處理函數中涉及DOM操作,則會導致瀏覽器掉幀,繼而導致動畫斷續顯示,畫面粘滯,在很大程度上影響用戶體驗。

如果在高頻事件以大於16.7ms/幀的速度進行,並且在該事件處理函數中進行大量的計算或DOM操作,會出現什麼問題?

由於該事件處理函數複雜且觸發過於頻繁,會導致上一次事件觸發的操作計算無法在下一次事件觸發前完成,則會使瀏覽器CPU使用率不斷增加,繼而造成瀏覽器卡頓甚至崩潰,如下圖所示:
example
註:註意觀察圖片左邊的控制台輸出以及右邊瀏覽器任務管理器的CPU使用率。

由上面的問題,我們可以知道,使用高頻事件時必須先解決其潛在的問題,才能保證頁面性能。針對以上問題,我們可用以下兩點方法從根本上上解決問題,如下:

  • 高頻事件處理函數,不應該含有複雜的操作,例如DOM操作和複雜計算(DOM操作一般會造成頁面迴流和重繪,使瀏覽器不斷重新渲染頁面,若有疑問可閱讀上文 – 瀏覽器渲染過程)。

  • 控制高頻事件的觸發頻率

控制高頻事件的觸發頻率是關鍵點,但是事件觸發是原生事件監聽方法進行監聽的,那麼我們該如何控制?

如果我們能延遲事件處理函數的執行,那麼就相當於控制了事件的觸發頻率,然後再通過保存執行狀態來控制事件處理函數的執行,那麼整個問題就可以迎刃而解了。

其中防抖和節流對高頻事件進行優化的原理就是通過延遲執行,將多個間隔接近的函數執行合併成一次函數執行。下麵將會詳細講解。

 

防抖(Debouncing)

針對高頻事件,防抖就是將多個觸發間隔接近的事件函數執行,合併成一次函數執行。

實現防抖的關鍵點主要有兩個,如下:

  • 使用setTimeout延時器,傳入的延遲時間,將事件處理函數延遲執行,並且通過事件觸發頻率與延遲時間值的比較,控制處理函數是否執行

  • 使用柯里化函數結合閉包的思想,將執行狀態保存在閉包中,返回新函數,在新函數中通過執行狀態控制是否在滾動時執行處理函數

實現代碼如下:

/*
* @param fn Function 事件處理函數
* @param delay Number 延遲時間
* @param isImmediate Boolean 是否滾動時立刻執行
* @return Function 事件處理函數
*/
var debounce = function(fn, delay, isImmediate) {
//使用閉包,保存執行狀態,控制函數調用順序
var timer;

return function() {
var _args = [].slice.call(arguments),
context = this;

clearTimeout(timer);

var _fn = function() {
timer = null;
if (!isImmediate) fn.apply(context, _args);
};

//是否滾動時立刻執行
var callNow = !timer && isImmediate;

timer = setTimeout(_fn, delay);

if(callNow) fn.apply(context, _args);
}
}

 

防抖技術使用如下:

var debounceScroll = debounce(function() {
//事件處理函數,滾動時進行的處理

}, 100)

window.addEventListener("scroll", debounceScroll)

 

防抖技術僅靠傳入延遲時間值的大小控制高頻事件的觸發頻率,如果傳入的延遲時間值比較大,那麼就會出現一定的問題。例如當傳入延遲時間為1000ms,那麼當用戶滾動速度大於1000ms/次時,則無論滑鼠滾動多久都不會觸發事件處理函數。因此防抖技術存在一定的缺陷,會不適用於某些場景,例如圖片懶載入。這個時候節流就派上用場了。

 

節流(Throttle)

節流也是將多個觸發間隔接近的事件函數執行,合併成一次函數執行,並且在指定的時間內至少執行一次事件處理函數。

節流實現原理跟防抖技術類似,但是比防抖多了一次函數執行判斷,實現的關鍵點是:

  • 利用閉包存儲了當前和上一次執行的時間戳,通過兩次函數執行的時間差跟指定的延遲時間的比較,控制函數是否立刻執行

實現代碼如下:

/*
* @param fn Function 事件處理函數
* @param wait Number 延遲時間
* @return Function 事件處理函數
*/
var throttle = function(fn, wait) {
var timer, previous, now, diff;
return function() {
var _args = [].slice.call(arguments),
context = this;
//儲存當前時間戳
now = Date.now();

var _fn = function() {
//存儲上一次執行的時間戳
previous = Date.now();
timer = null;
fn.apply(context, _args)
}

clearTimeout(timer)

if(previous !== undefined) {
//時間差
diff = now - previous;
if(diff >= wait) {
fn.apply(context, _args);
previous = now;
} else {
timer = setTimeout(_fn, wait);
}
}else{
_fn();
}
}
}

 

註:以上防抖和節流函數封裝是根據個人理解進行封裝的,若想對比不同的封裝方法,建議閱讀第三方underscore函數庫中的throttle和debounce的實現方法,原理大致是一樣的,但是封裝思維稍有不同。

拓展:

以上的節流和防抖技術都是用setTimeout實現的,是否有其他的實現方案,性能是否會更好?

可直接使用瀏覽器幀頻刷新自動調用的方法(requestAnimationFrame)實現,實現起來會更加簡單,而且性能會更好,但是唯一缺點就是需要自行解決低版本的IE瀏覽器相容問題,實現代碼如下:

//解決requestAnimationFrame相容問題
var raFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};

//柯里化封裝
var rafThrottle = function(fn) {
var isLocked;
return function() {
var context = this,
_args = arguments;

if(isLocked) return

isLocked = true;
raFrame(function() {
isLocked = false;
fn.apply(context, _args)
})
}
}

 

 

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) {
if(typeof fn !== "function"){
throw TypeError("fn is not function.");
}
//復用第一個參數
var args = [].slice.call(arguments, 1);
//返回新函數
return function(){
//收集剩餘參數
var _args = [].slice.call(arguments);
//返回結果
return fn.apply(this, args.concat(_args));
}
}

 

柯里化函數的特點如上註釋所示:

  • 復用第一個參數

  • 返回新函數

  • 收集剩餘參數

  • 返回結果

柯里化函數的簡單例子應用,如下:

//add(19)(10, 20, 30),求該函數傳遞的參數和

var add = createCurry(function() {
//獲取所有參數
var args = [].slice.call(arguments);

//返回累加結果
return args.reduce(function(accumulator, currentValue) {
return accumulator + currentValue
})
}, 19)

add(10, 20, 30); //79

 

 

總結

以上便是柯里化函數的基本應用以及原理,希望可以提升大家對柯里化函數以及函數式編程的理解,如有錯誤,敬請指正。

[鏈接](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/)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對剛做的東西記個筆記 如果遇到同樣問題解決起來又問題的歡迎留言 var emailtext = $("#TextBoxEmail").val();//獲得要截取的值 var arr = emailtext.split("@");//截取郵箱字元串 var emailtype = arr[1];//郵 ...
  • 快9點了,就抓緊寫博客吧,早點睡,還是得11點之前睡覺。 下午的時候一直在調試,晚上因為工作的事情,耽誤了一下,不過說個好消息吧,我找到工作了,不過是老家的,看來省城是留不住了。 現在趕時間,就直接是把老師的代碼拿過來用,寫點註釋,這樣效率快一點兒。 上面說是要裝vue-avatar/dist/av ...
  • jsp中引入: <OBJECT id=WebBrowser classid=CLSID:8856F961-340A-11D0-A96B-00C04FD705A2 height=0 width=0></OBJECT> jsp中引入樣式: 法二:直接全部引進去,做相關內容的替換(有提示!)調用方法即可 ...
  • 前言 小程式開發的過程中,如果你涉及到文件的上傳,就需要使用微信提供的API去上傳文件: 官方文檔的解釋這裡就不多介紹了,主要看一下這個方法具體如何使用以及為什麼這樣使用。 正文 我們可以先看一下該API的參數說明: 其實wx.uploadFile的操作是你把要請求的數據以及要請求的伺服器URL傳遞 ...
  • 我們在做數據提交的時候經常用到表單驗證,如果遇到表單元素有沒填的選項,一般都會禁止表單提交 如果表單需要驗證的數據比較多,有些必填的欄位為空 提交不了 但是沒有定位到未填項的位置 導致用戶懵逼 不知道為什麼提交不了 這個時候,我們可以給未填的表單項加foucs() 例如上圖的代碼,這樣游標就可以定位 ...
  • 0. 瀏覽器渲染原理: 1. 瀏覽器宿主環境層面: 2. 網路層面: 3. 代碼層面: ...
  • 原型鏈是js面向對象的基礎,非常重要。 一,創建對象的幾種方法: 1,字面量 var o1 = { name:'o1' }; 2,構造函數 var M = function(name){ this.name = name; }; var o2 = new M('o2'); var a = {} 其實 ...
  • 1.模板名片發送後不顯示內容?(如第一張圖) 經過查看官方文檔,是data數據格式問題,小程式端傳給後端的data數據被服務端解析出了一點問題(data裡面的字元串加入了"\")。現在後端將數據從新做了清洗。已解決。解決後的展示如第二張圖。 2.上傳圖片一直失敗。 解決答案相關鏈接:https:// ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...