柯里化 curring 是把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下参数并且返回结果的新函数的技术。
柯里化 -> 部分求值,返回接收剩余参数且返回结果的新函数。
特点:
参数复用 - 复用最初函数的第一个参数
提前返回 - 返回接收余下的参数且返回结果的新函数
延迟执行 - 返回新函数,等待执行
应用
兼容浏览器事件监听方法
性能优化:防抖和节流
兼容低版本IE bind方法等
事件监听 1 2 3 4 5 6 7 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 进行一次兼容判断,其实这个判断是无必要的,用柯里化优化后如下:
1 2 3 4 5 6 7 8 9 10 11 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 针对高频事件,防抖就是讲多个触发间隔接近的事件函数执行,合并成一次函数执行
实现防抖的关键点有两个:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 节流也是将多个触发间隔接近的事件函数执行,合并成一次函数执行,并且在指定时间内执行执行一次事件处理函数。
节流实现原理跟防抖类似,但是比防抖多了一次实函数执行判断,实现的关键点是:
利用闭包存储了当前和上一次执行的时间戳,通过两次函数执行的时间差跟指定的延迟事件的比较,控制函数是否执行
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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方法封装,不考虑构造函数等,实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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兼容方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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, aArgs.concat (Array .prototype .slice .call (arguments )) ) } if (this .prototype ) { fNOP.prototype = this .prototype } fBound.prototype = new fNOP () return fBound } }
要理解复杂的bind兼容方法,必须彻底理解以下四个基础知识
JS原型对象
构造函数是用new操作符的过程
this的指向问题
熟悉bind方法的是用场景
柯里化函数的封装 分析了柯里化的各种是用场景,我们来尝试一下封装一个简单的柯里化函数,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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)) } }
柯里化函数的特点如上注释所示:
复用第一个参数
返回新函数
收集剩余参数
返回结果