JavaScript函数式编程实战:计算平均值与标准差

作者:函数茶馆时间:2026-10-29 08:53:19浏览:290

本文通过JavaScript计算数组平均值与标准差的实例,深入解析函数式编程核心概念。文章对比了命令式与函数式两种写法,详细拆解了reduce、map、partial及compose等关键函数的用法与组合逻辑,帮助开发者理解如何利用高阶函数构建清晰的数据处理管道。

JavaScript函数式编程基础与实例

本文通过具体实例解析JavaScript中的函数式编程方法,旨在为读者提供可参考的代码实践与理论分析。

JavaScript与其他动态语言类似,支持高阶函数的编写。高阶函数是指能够接收函数作为参数或返回函数的函数。由于在JavaScript中函数被视为完全的对象,属于第一类公民,这为实施函数式编程提供了基础条件。

非函数式与函数式实现对比

以下示例代码源自某JavaScript教程,其目标是计算数组元素的平均值和标准差。首先展示一种非函数式(命令式)的编程实现方式:


var data = [1,1,3,5,5];
var total = 0;
for(var i = 0;i < data.length;i++)
  total += data[i];
var mean = tatal/data.length; //平均数为3
//计算标准差
total = 0;
for(var i = 0;i < data.length;i++){
  var deviation = data[i] - mean;
  tatal += deviation * deviation;
  }
var stddev = Math,.sqrt(total/(data.length-1));//标准差为2

为了采用函数式编程风格,我们需要预先定义一系列辅助函数(helper functions):


//将类数组对象转换为真正的数组
function array(a,n){
 return Array.prototype.slice.call(a,n||0);
}
//将函数实参传递至左侧
function partial_left(f){
 var args = arguments;
 return function(){
  var a = array(args,1);
  a = a.concat(array(arguments));
  return f.apply(this,a);
 };
}
//将函数的实参传递至右侧
function partial_right(f){
 var args = arguments;
 return function(){
  var a = array(arguments);
  a = a.concat(array(args,1));
  return f.apply(this,a);
 };
}
//该函数实参被用做模版,
//实参列表中的undefined值会被实际实参值填充。
function partial(f){
 var args = arguments;
 return function(){
  var a = array(args,1);
  var i = 0,j = 0;
  for(;i<a.length;i++)
   if(a[i] === undefined)
    a[i] = arguments[j++];
  a = a.concat(array(arguments,j));
  return f.apply(this,a);
 };
}
//返回一个函数类似于f(g())
function compose(f,g){
 return function(){
  return f.call(this,g.apply(this,arguments));
 };
}

接下来展示完全使用函数式编程风格编写的JavaScript代码:


var data = [1,1,3,5,5];
var sum = function(x,y){return x+y;};
var product = function(x,y){return x*y;};
var neg = partial(product,-1);
var square = partial(Math.pow,undefined,2);
var sqrt = partial(Math.pow,undefined,0.5);
var reciprocal = partial(Math.pow,undefined,-1);
//好吧,高潮来鸟 :)
var mean = product(reduce(data,sum),reciprocal(data.length));
var stddev = sqrt(product(reduce(map(data,compose(square,partial(sum,neg(mean)))),sum),reciprocal(sum(data.length,-1))));

核心函数reduce与map解析

除了reduce和map这两个核心函数外,其他辅助函数已在前面定义。其中,reduce函数的行为类似于Ruby语言中的inject函数:


ary = (1..10).to_a
ary.inject(0) {|sum,i|sum + i} //结果为55

JavaScript的具体实现代码如下:


var ary = [1,2,3,4,5,6,7,8,9,10]
ary.reduce(function(sum,i){
 return sum + i;
},0);

参数0表示sum的初始值。如果省略该参数,sum将初始化为数组的第一个元素值,此处可以省略不写。

map函数的作用相对直观,它类似于对数组中的每个元素执行操作,并返回一个经过处理的新数组。以Ruby代码为例,其JavaScript实现方式与之类似:


a = (1..3).to_a; #数组[1,2,3]
a.map {|x| x*2} #返回新数组[2,4,6]

复杂函数组合逻辑深度拆解

下面我们来详细分析前面那串较长的函数式代码。

sum和product分别定义了元素相加和相乘的逻辑函数;

neg也是一个函数,其功能等价于调用product(-1, x),即对x的值求负;

square函数等价于Math.pow(x, 2),用于计算x的平方值。注意这里partial函数的第二个参数为undefined,这意味着该形参将由第一个实参填补。换句话说,square(x)的功能等同于Math.pow(x, 2)。

sqrt函数与square类似,功能等价于Math.pow(x, 0.5),即计算x的平方根。最后一个函数reciprocal也不复杂,等价于Math.pow(x, -1),即计算x的负一次方,也就是x的倒数。

接下来展示如何将上述各个函数组合在一起使用:

首先看平均值的计算,逻辑很简单:先计算数组元素的总和,然后乘以数组长度的倒数,即数组总和除以数组长度。

最后来看看似复杂的标准差计算,建议由内向外逐步分析: 首先观察包含neg的那一层逻辑:


//等价于函数sum(-1 * mean + x)
partial(sum,neg(mean)

接下来分析compose函数的作用:


//下面在源代码上做了等价替换,可以再次等价于:
//square(sum(-1*mean + x)),再次展开(我剥,我剥,我剥洋葱...):
//Math.pow(sum(-1*mean + x),2);
compose(square,sum(-1*mean + x))

随后查看map函数的执行过程:

//很清楚吧!?即data中每一个元素都为一个x,将其传入后面的函数,然后返回一个计算后的新数组,即新数组中的每个元素的值是data中的每个元素加上data负的平均数,然后对其结果计算2次方的结果。


map(data,Math.pow(sum(-1*mean + x),2))

接着看map外层的reduce函数:


//将前面新数组的每个元素值加起来。
reduce(map(...),sum)

然后查看reciprocal函数的具体表现:


//等价于求(data.length-1)的倒数
reciprocal(sum(data.length,-1))

再看外层的product函数:


//等价于新数组元素的和除以(data.length-1)
product(reduce(...),reciprocal(...))

最外层的sqrt表示对前述除法得出的结果求平方根。大家可以对照前文非函数式编程的代码,两者逻辑完全一致。看似庞大的一坨代码,展开分析后难度立即降至零。如果读者仍感困惑,那完全是表达能力的局限,欢迎提问交流。

解释完毕,代码分析到此结束。

希望本文所述的JavaScript程序设计内容能对读者有所帮助。

作者最新文章