Skip to content

柯里化

柯里化 curring 是把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下参数并且返回结果的新函数的技术。

<img src="http://blog.fueson.top/article/img/curry.jpg">

<!-- more -->

柯里化 -> 部分求值,返回接收剩余参数且返回结果的新函数。

特点:

  • 参数复用 - 复用最初函数的第一个参数
  • 提前返回 - 返回接收余下的参数且返回结果的新函数
  • 延迟执行 - 返回新函数,等待执行

应用

  • 兼容浏览器事件监听方法
  • 性能优化:防抖和节流
  • 兼容低版本IE bind方法等

事件监听

js
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 进行一次兼容判断,其实这个判断是无必要的,用柯里化优化后如下:

js
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延时器,传入延迟时间,将事件处理函数延迟执行,并且通过事件触发频率与延迟事件值比较,控制处理函数是否执行

  • 使用柯里化函数结合闭包思想,将执行状态保存在闭包中,返回新函数,在新函数中通过执行状态控制是否在滚动时执行处理函数

代码如下:

js
/*
 * @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

节流也是将多个触发间隔接近的事件函数执行,合并成一次函数执行,并且在指定时间内执行执行一次事件处理函数。

节流实现原理跟防抖类似,但是比防抖多了一次实函数执行判断,实现的关键点是:

  • 利用闭包存储了当前和上一次执行的时间戳,通过两次函数执行的时间差跟指定的延迟事件的比较,控制函数是否执行

代码如下:

js
/*
* @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

js
// 解决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方法封装为两种情况:

  1. 简单的bind方法封装,不考虑构造函数等,实现如下:
js
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))
    }
  }
}
  1. 复杂情况,考虑bind的任何用法,这里直接是用MDN的bind兼容方法:
js
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方法的是用场景

柯里化函数的封装

分析了柯里化的各种是用场景,我们来尝试一下封装一个简单的柯里化函数,如下:

js
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))
  }
}

柯里化函数的特点如上注释所示:

  • 复用第一个参数
  • 返回新函数
  • 收集剩余参数
  • 返回结果

共 20 个模块,1301 篇 Markdown 文档。