(轉載)柯里化函數應用

来源: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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...