03-2 高阶函数

225 阅读8分钟

读书笔记:JS设计模式与开发实践
高阶函数是指至少满足下列条件之一的函数

  • 函数可作为参数被传递
  • 函数可作为返回值输出

1 函数作为参数传递

把函数当作参数传递,这代表我们可抽离出一部分容易变化的业务逻辑,把这部分逻辑放在函数参数中,这样一来可分离业务代码中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。

1-A 回调函数

回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,委托给另外一个函数来执行。

比如,我们想在页面中创建100个div节点,然后把这些div节点都设置为隐藏。

var appendDiv = function(){
  for (let i = 0; i < 100; i++) {
    var div = document.createElement('div')
    div.innerHTML = i;
    document.body.appendChild('div')
    div.style.display='none'
  }
}
appendDiv()

把div.style.display='none'的逻辑硬编码在appendDiv里显然是不合理的,难以复用。 于是可把div.style.display='none'抽出来,用回调函数的形式传入

var appendDiv = function(callback){
  for (let i = 0; i < 100; i++) {
    var div = document.createElement('div')
    div.innerHTML = i;
    if (typeof callback === 'function') {
      callback(div)
    }
  }
}
appendDiv(function(node){
  node.style.display='none'
})

隐藏节点的请求实际上是由客户发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏需求放在回调函数中,委托给appendDiv方法。

1-B Array.prototype.sort

Array.prototype.sort接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。从Array.prototype.sort的使用可看到,我们的目的是对数组进行排序,这是不变的部分; 而使用什么规则去排序,则是可变的部分。把可变的部分封装在函数参数里,动态传入是非常灵活的方法

[1, 4, 3].sort(function(a,b){
  return a - b
})

2 函数作为返回值输出

相比把函数当作参数传递,函数当作返回值输出的应用场景也许更多,也更能体现函数式编程的巧妙。让函数继续返回一个可执行的函数,意味着运算过程是可延续的。

2-A 判断数据的类型

var isString = function(obj){
  return Object.prototype.toString.call(obj) === '[object String]'
}

var isArray = function(obj){
  return Object.prototype.toString.call(obj) === '[object Array]'
}

var isNumber = function(obj){
  return Object.prototype.toString.call(obj) === '[object Number]'
}

这些函数的大部父实现都是相同的,不同的只是返回字符串,我们尝试把这些字符串作为参数提前值入isType函数

var isType = function(type){
  return function(obj){
    return Object.prototype.toString.call(obj) === `[object ${type}]`
  }
}

var isString = isType('String')
var isArray = isType('Array')
var isNumber = isType('Number')

还可用循环语句,批量注册这些isType函数

var Type = {}
for (let i = 0, type; type = ['String', 'Array', 'Number'][i++];) {
  (function(type){
    Type['is'+ type] = function(obj){
      return Object.prototype.toString.call(obj) === `[object ${type}]`
    }
  })(type)
}

Type.isArray([])
Type.isString('str')

2-B getSingle

下面一个单例模式

var getSingle = function(fn){
  var ret;
  return function(){
    return ret || (ret = fn.apply(this, arguments))
  }
}

这个高阶函数的例子,即把函数当作参数传递,又让函数执行返回了另外一个函数。我们可以看看getSingle函数的效果

var getScript = getSingle(function(){
  return document.createElement('script')
})

var script1 = getScript()
var script2 = getScript()

console.log(script1 === script2);

3 高阶函数实现AOP

AOP面向切面编程的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可保持业务逻辑模块的纯净和高内聚性,其次是可很方便地复印日志统计等功能模块。

在JS中AOP是与生俱来的能力

通常,在JS中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中具体实现技术有很多,本节通过扩展Function.prototype来做到这一点

Function.prototype.before = function(beforefn){
  var __selt =this;
  return function(){
    beforefn.apply( this, arguments)
    return __self.apply(this, arguments)
  }
}

Function.prototype.after = function(afterfn){
  var __self = this;
  return function(){
    var ret = __self.apply(this, arguments)
    afterfn.apply(this,arguments)
    return ret;
  }
}

var func = function(){
  console.log(2);
}

func = func.before(function(){
  console.log(1);
}).after(function(){
  console.log(3);
})

func()

4 高阶函数的其他应用

4-A currying

函数柯里化又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

假设我们编写一个记账本

var monthlyCost = 0;
var cost = function(money){
  monthlyCost += money;
}

cost(100)
cost(200)
cost(300)

console.log(monthlyCost);  // 600

每天都只是保存开销,直到每月最后一天进行求值,这就达到了我们的目的。 下面是一个不完整的实现

const cost = (function(){
  const args = []

  return function() {
    if(arguments.length === 0){
      var money = 0;
      for (let i = 0; i < args.length; i++) {
        money += args[i]
      }
      return money;
    }else{
      [].push.apply(args, arguments)
    }
  }
})()


cost(100)   // 存值
cost(200)   // 存值
cost(300)   // 存值
console.log(cost());  // 求值

在一个通用的currying

var currying = function (fn) {
  var args = []
  return function () {
    if (arguments.length === 0) {
      // return fn.apply(this, args)
      return fn(args)
    } else {
      args.push(...arguments)
    }
  }
}
var cost = (function () {
  var money = 0;
  return function (args) {
    for (let i = 0; i < args.length; i++) {
      money += args[i]
    }
    return money
  }
})()

var cost = currying(cost)

cost(100)   // 存值
cost(200)   // 存值
cost(300)   // 存值
console.log(cost());  // 求值

4-B uncurrying

在JS中,当我们调用对象的某个方法时,其实不用关心该对象是否被设计为拥有这个方法,这个动态类型语言的特点,也是常说的鸭子类型思想。

同理,一个对象也未必只能使用自身的方法,也可借用本不属于它的方法,即call和apply。

var obj1 = {
  name: 'sven'
}

var obj2 = {
  getName: function(){
    return this.name
  }
}

console.log(obj2.getName.call(obj1));

我常用说类数组借用Array.prototye方法

(function () {
  Array.prototype.push.call(arguments, 4)
  console.log(arguments);
  
})(1,2,3)

那有办法把泛化this的过程提取出来呢?

Function.prototype.uncurrying = function(){
 var self = this;  // self 此时是 Array.prototype.push
 return function(){
   var obj = Array.prototype.shift.call(arguments)
   console.log('obj==>', obj);
   // boj是 { "length": 1, "0": 1 }
   return self.apply(obj, arguments)
   // 相当于 Array.prototype.push.apply(obj, 2)
 }
}

var push = Array.prototype.push.uncurrying()

var obj = {
 'length': 1,
 "0": 1
}

push(obj, 2);
push(obj, 3);
console.log(obj);

4-C 函数节流

JS中的函数多数是由用户主动调用的,但少数情况下,函数不是由用户直接调用的,函数有可能被频繁调用,而造成性能问题。

函数被频繁调用场景

  1. window.onresize事件
  2. mousemover事件
  3. 上传进度提示

函数节流的原理

上面三个场景的共同点是函数被触发的频率太高,需要我们按时间段来忽略掉一些事件请求。显然setTimeout可解决此类问题

函数节流代码实现

const throttle = function(fn, interval = 500){
  const __self = fn;              // 保存需要被延迟执行的函数引用
  let timer;                      // 定时器
  let firstTime = true;           // 是否第一次调用

  return function () {
    let args = arguments;
    let __me = this;

    if (firstTime) {              // 第一次调用不延迟
      __self.apply(__me, args)
      return firstTime = false;
    }

    if(timer){                    // 如果定时器还在,说明前一次延迟执行还没有完成
      return false;
    }

    timer = setTimeout(function () {  // 延迟执行
      clearTimeout(timer)
      timer = null;
      __self.apply(__me, args)
    }, interval)
  }
}

window.onresize = throttle(function () {
  console.log(1);
}, 500)

4-D 分时函数

另一个问题是,某此函数确实是用户主动调用的,但这些函数会影响性能。如创建qq好友列表,在短时间内往页面中大量添加DOM节点显然会让浏览器吃不消

const ary = []

for (let i = 0; i < 1000; i++) {
  ary.push(i)
}

const renderFirendList =  (data) => {
  for (let i = 0; i < data.length; i++) {
    const div = document.createElement('div')
    div.innerHTML = i;
    document.body.appendChild(div)
    
  }
}
renderFirendList(ary)

这个问题解决方案之一是timeChunk函数

const timeChunk = function(ary, fn, count){
  var obj;
  var t;

  var len = ary.length;
  var start = function(){
    for (let i = 0; i < Math.min(count || 1, ary.length); i++) {
      var obj = ary.shift();
      fn(obj)
    }
  }
  return function(){
    t = setInterval(function(){
      if(ary.length === 0){
        return clearInterval(t)
      }
      start()
    }, 200)
  }
}

最后我们利用timeChunk函数,每批只创建8个节点

var ary = []
for (let i = 0; i < 1000; i++) {
  ary.push(i)
}

var renderFirendList = timeChunk(ary, function (n) {
  var div = document.createElement('div')
  div.innerHTML = n
  document.body.appendChild(div)
}, 8)

renderFirendList()

4-E 惰性加载函数

在web开发中,一些嗅探工作不可避免。比如需要一个通用addEvent事件绑定函数,常见写法如下

var addEvent = function (elem, type, handler) {
  if (window.addEventListener) {
    return elem.addEventListener(type, handler, false)
  }
  if (window.attachEvent) {
    return elem.attachEvent(`on ${type}`, haldler)
  }
}

这个函数缺点是,每次调用都会执行里面的if。

第二种方案

var addEvent = (function(){
  if (window.addEventListener) {
    return function (elem, type, handler) {
      elem.addEventListener(type, handler, false)
    }
  }
  if (window.attachEvent) {
    return function (elem, type, handler) {
      elem.attachEvent(`on ${type}`, handler)
    }
  }
})()

目前addEvent依然有个缺点,也写我们从未使用过addEvent函数,这样看来,前一次的嗅探是多余操作,且这也会稍稍延长ready时间。

第三种方案即是我们讨论的惰性载入函数方案。此是addEvent依然被声明为一个普通函数,内部依然有分支。但是在第一次进入分支后,函数内部会重写这个函数,重写后的函数 就是我们期望的addEvent函数。之后进入addEvent不再存在条件分支语句


var addEvent = function (elem, type, handler) {
  if (window.addEventListener) {
    addEvent = function (elem, type, handler) {
      elem.addEventListener(type, handler, false)
    }
  }else if(window.attachEvent){
    addEvent = function(elem, type, handler){
      elem.attachEvent(`on ${type}`, handler)
    }
  }
  addEvent(elem, type, handler)
}

var div = document.getElementById('div1')
addEvent(div, 'click', function(){
  console.log(1);
})
addEvent(div, 'click', function(){
  console.log(2);
})