JavaScript高阶函数实战:回调、偏函数与柯里化详解

作者:半夏

时间:2026-10-29

浏览:141

本文深入解析JavaScript中函数作为一等公民的高阶应用。通过回调函数实现代码复用,利用偏函数简化类型判断,结合柯里化实现部分求值。同时介绍事件节流与防抖技术,优化频繁触发事件的执行频率,提升Web应用性能。

文章目录

JavaScript高阶函数基础

在常规编程语言中,函数的参数通常仅限于基本类型或对象引用,返回值也多为基本数据类型或对象引用。然而,在JavaScript中,函数被视为一等公民,既可作为参数传递,也可作为返回值返回。所谓高阶函数,即指那些接收函数作为参数或将函数作为返回值返回的函数。这两种特性在实际开发中拥有广泛的应用场景,本文总结了工作学习中遇到的几种典型应用。

回调函数与代码复用

回调函数

代码复用是评估应用程序质量的重要标准之一。通过将易变的业务逻辑封装在回调函数中,可以有效提升代码的复用率。例如,ES5为数组新增的forEach方法,用于遍历数组并对每个元素调用同一个函数。


array = {};
array.forEach = function(arr, fn){
  for (var i = 0, len = arr.length; i < len; i++) {
    fn(arr[i], i, arr);
  }
}

借助回调函数,可以将业务重点聚焦于回调逻辑本身,无需每次重复编写遍历代码。

偏函数与类型判断

偏函数

将函数作为返回值输出是偏函数的典型应用。偏函数是指创建一个新函数,该函数调用另一个已预置部分参数或变量的函数。尽管定义晦涩难懂,我们通过实例来理解,偏函数最典型的场景是类型判断。

JavaScript对象拥有三个属性:原型属性、类属性和可扩展性(不了解的读者可参考《JavaScript高级程序设计》第138页)。类属性是一个字符串,JavaScript并未直接提供,但我们可以利用Object.prototype.toString间接获取。该函数总是返回如下格式:

[object Class]

因此,我们可以编写一系列isType函数。

代码如下:

 


isString = function(obj){
  return Object.prototype.toString.call(obj) === "[object String]";
}
isNumber = function(obj){
  return Object.prototype.toString.call(obj) === "[object Number]";
}
isArray = function(obj){
  return Object.prototype.toString.call(obj) === "[object Array]";
}

上述函数中大部分代码重复,此时高阶函数便派上用场:


isType = function(type) {
  return function(obj) {
    return Object.prototype.toString.call(obj) === "[object " + type + "]";
  }
}

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

因此,通过指定部分参数来返回一个新的定制函数形式即为偏函数。

柯里化与部分求值

currying(柯里化)

currying又称部分求值。一个currying函数首先接收一些参数,接收后并不立即求值,而是继续返回另一个函数,之前传入的参数保存在形成的闭包中。当函数真正需要求值时,之前传入的所有参数将一次性用于求值。


var currying = function(fn) {
  var args = [];
  
  return function() {
    if (arguments.length === 0) {
      return fn.applay(this, args);
    } else {
      args = args.concat(arguments);
      return arguments.callee;
    }
  }
}

假设我们以计算一个月每天花销为例:


var currying = function(fn) {
debugger;
  var args = [];
  
  return function() {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      Array.prototype.push.apply(args, arguments);
      return arguments.callee;
    }
  }
}

cost = function(){
  var sum = 0;
  for (var i = 0, len = arguments.length; i < len; i++) {
    sum += arguments[i];
  }
  
  return sum;
}
var cost = currying(cost);

cost(100);
cost(200);
alert(cost())

事件节流与防抖处理

事件节流

在某些场景下,某些事件可能会被重复触发,但事件处理函数并不需要每次都执行。例如在window.resize事件中进行复杂逻辑计算,如果用户频繁改变浏览器大小,复杂计算会对性能造成严重影响;有时这些逻辑计算并不需要每次resize时都触发,只需计算有限几次即可。这时我们需要根据时间段来忽略一些事件请求。请看以下节流函数:


function throttle(fn, interval) {
   var doing = false;

   return function() {
    if (doing) {
     return;
    }
    doing = true;
    fn.apply(this, arguments);
    setTimeout(function() {
     doing = false;
    }, interval);
   }
  }
  
  window.onresize = throttle(function(){
    console.log('execute');
  }, 500);

通过控制函数执行时间,可以在函数执行次数与功能需求之间达到平衡。另一个事件是mousemove。如果我们给一个DOM元素绑定该事件,鼠标在该元素上移动时,该事件便会重复触发。

事件结束

对于某些可以频繁触发的事件,有时我们希望在事件结束后进行一系列操作。这时我们可以利用高阶函数做如下处理:

 


function debounce(fn, interval) {
  var timer = null;

 function delay() {
  var target = this;
  var args = arguments;
  return setTimeout(function(){
   fn.apply(target, args);
  }, interval);
 }

 return function() {
  if (timer) {
   clearTimeout(timer);
  }

  timer = delay.apply(this, arguments);
 }
};
window.onresize = throttle(function(){
  console.log('resize end');
}, 500);

如果在这一过程中事件被触发,则清除上一次事件句柄,重新绑定执行时间。

参考资料:

《深入浅出node》

《Javascript设计模式与开发实践》

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章