【JS】函数柯里化
柯里化 curring 是把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下参数并且返回结果的新函数的技术。
柯里化 -> 部分求值,返回接收剩余参数且返回结果的新函数。
特点:
- 参数复用 - 复用最初函数的第一个参数
- 提前返回 - 返回接收余下的参数且返回结果的新函数
- 延迟执行 - 返回新函数,等待执行
应用
- 兼容浏览器事件监听方法
- 性能优化:防抖和节流
- 兼容低版本IE bind方法等
事件监听
var addEvent = function(elm, type, fn, isCapture) { if (window.addEventListener) { elm.addEventListner(type, fn, isCapture) } else if (window.attachEvent) { elm.attachEvent('on' + type, fn) }}当我们调用addEvent方法时,都会执行if else-if 进行一次兼容判断,其实这个判断是无必要的,用柯里化优化后如下:
var addEvent = function(elm, type, fn, isCapture) { if (window.addEventListener) { return function(elm, type, fn, isCapture) { elm.addEventListen(type, fn, isCapture) } } else if (window.attachEvent) { return function(elm, type, fn) { elm.attachEvent('on' + type, fn) } }}该例就利用了柯里化提前返回和执行的特点:
- 提前返回 - 使用函数立即调用进行一次兼容判断,返回兼容的事件绑定方法
- 延迟执行 - 返回新函数,在新函数调用兼容的事件方法。等待addEvent新函数调用,延迟执行
防抖和节流
web开发中,事件resize, scroll, mousemove等属于高频事件。浏览器页面渲染帧率为60fps,大约16.67ms刷新一帧。 若事件触发频率大于显示帧率,则会发生掉帧、卡顿等现象,更坏地浏览器直接崩溃。
要解决高频事件的问题,其根本在于:
- 高频事件处理函数,不应该含有复杂操作,如DOM操作和复杂计算(DOM操作一般会造成页面回流和重绘,使浏览器不断重新渲染页面)
- 控制高频事件的触发频率
其中防抖和节流对高频事件进行优化的原理就是通过延迟执行,将多个间隔接近的函数执行合并成一次函数执行。
防抖 debounce
针对高频事件,防抖就是讲多个触发间隔接近的事件函数执行,合并成一次函数执行
实现防抖的关键点有两个:
-
使用setTimeout延时器,传入延迟时间,将事件处理函数延迟执行,并且通过事件触发频率与延迟事件值比较,控制处理函数是否执行
-
使用柯里化函数结合闭包思想,将执行状态保存在闭包中,返回新函数,在新函数中通过执行状态控制是否在滚动时执行处理函数
代码如下:
/* * @param fn Function 事件处理函数 * @param delay Number 延迟时间 * @param isImmediate Boolean 是否滚动时立刻执行 * @return Function 事件处理函数 */var debound = 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)防抖技术仅靠传入延迟时间值的大小控制高频事件的触发频率。如果传入的延迟事件较大,则可能导致不触发事件处理函数,这时节流就派上用场了
节流 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() } }}拓展:节流和防抖都是用setTimeout实现的,改用window.requestAnimationFrame
实现起来更简单,性能更好,但不支持低版本IE
// 解决requestAnimationFrame兼容var reFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
// 柯里化封装var refThrottle = function(fn) { var isLocked return function() { var context = this var _args = arguments
if (isLocked) return
isLocked = true reFrame(function() { isLocked = false fn.apply(context, _args) }) }}bind柯里化函数
函数的bind方法我们不陌生,但在低版本IE不兼容,若要实现兼容其关键点在于:
- 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') { 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方法的是用场景
柯里化函数的封装
分析了柯里化的各种是用场景,我们来尝试一下封装一个简单的柯里化函数,如下:
function createCurry(fn) { if (typeof fn !== 'function') { throw TypeError('fn is not a function') } // 复用第一个参数 var args = [].slice.call(arguments, 1) // 返回新函数 return function() { // 收集剩余参数 var _args = [].slice.call(arguments) // 返回结果 return fn.apply(this, args.concat(_args)) }}柯里化函数的特点如上注释所示:
- 复用第一个参数
- 返回新函数
- 收集剩余参数
- 返回结果
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!