JavaScript小特性(6)——函数式编程

首先说说什么事“函数式编程”,借用维基百科的概念:

函数式编程是种编程范型,它将电脑运算视为函数的计算。函数式编程的重点是函数的定义而不是像命令式编程那样强调状态机(state machine)的实现。

也就是说,函数式编程只描述在程序输入上执行的操作,重点是捕捉 “是什么以及为什么”,而不是 “如何做”,我们只需知道一个函数能返回什么样的结果,然后将结果用于进一步的运算。

有一个容易误解的概念是——“函数式编程就是一堆函数”,这是错误的。并不是一个语言支持函数,这个语言就可以叫做 “ 函数式语言 ” 。函数式语言中的 “ 函数( funct ion ) ” 除了能被调用之外,还具有一些其它的性质:

  • 函数是运算元
  • 在函数内保存数据
  • 函数内的运算对函数外无副作用

 

说回JavaScript,其实JavaScript并不是一个函数式编程语言,只能说它的实现参照了些函数式编程的特性,是个“半函数式编程语言”。下面我就介绍下JavaScript中函数式编程的一些特性吧。

 

一、函数(Function)是一等公民

Function是JavaScript中最基础的模块,本身为一种特殊对象(Object),属于顶层对象,不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,Function是依附于对象的,属于对象的一部分。JavaScript中一切皆是对象,那Function自然也是对象,换个角度说,一切皆是可传入Function的值,连Function本身也不例外。

这样有什么好处?举一个排序的例子:

//Array对象的sort方法,需传入一比较函数
var myarray = [2,5,7,3];
var byAsc = function(x,y) { return x-y; };
var byDesc = function(x,y){ return y-x; };
myarray.sort(byDesc);
alert(myarray);    //7,5,3,2
myarray.sort(byAsc);
alert(myarray);    //2,3,5,7

甚至于我们根本不需要定义一个对外公开的Function(因为其他地方不会使用到),直接用一个匿名函数:

//对一个日期数组排序
dateArr.sort( function (x,y) {
    return x.date – y.date;
});

 

二、高阶函数

高阶函数即为对函数的进一步抽象,上面提到的sort既是JS引擎自身提供的一个高阶函数。sort传入的比较函数(byAsc, byDesc)是没有任何预先的假设的,sort是对整个排序方法的二阶抽象,因此称之为“高阶”函数。

下面再举一个高阶函数的例子,数组元素遍历:

Array.prototype.each = function(fun){
    var ret = [], len = this.length, i;
    for(i = 0; i < len ; i++){
        ret.push(fun(this[i],i));
    }
    return ret;
}
alert([1,2,3].each(function(x){return x+1;}));  //[2,3,4]
alert([1,2,3].each(function(x){return x*2;}));  //[2,4,6]

代码重用什么的好处我就不多说了,最重要一个好处就是写起来爽!

 

三、闭包

这个特性对于初学者来说可能还真不好了解,详情可以看看我之前写的《JavaScript的闭包与作用域链》。举个闭包的小例子:

//实现一个计数器
function counter(){
    var n = 0;
    return function(){ return ++n; }
}
//创建一个计数器
var count1 = counter();
count1();  //1
count1();  //2
count1();  //3
//再创建一个
var count2 = counter();
count2();  //1
count2();  //2

闭包的最大特性就是不需要通过传递变量的方式就可以从内层直接访问外层的环境,这为多重嵌套下的函数式程序带来了极大的便利性。

有了闭包就相当于我们可以在函数内保存数据了,这样有啥好处呢?

 

四、函数柯里化(Currying)

啥是柯里化?详见维基百科

在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

有了“闭包”这个利器之后,我们就可以享受“柯里化”这种酷爽编程体验了,话不多说,上例子:

//这是一个Ajax页面局部刷新的例子

//替换DOM中某个节点的html
function update(id){
    return function (data){
        $("div#"+id).html(data.text);
    }
}

//Ajax局部刷新
function refresh(url, callback){
    $.ajax({
        type:"get",
     url:url,
        dataType:"json",
     success:  function (data){
            callback(data);
    });
}

//刷新两个区域
refresh("friends.php", update("friendsDiv"));
refresh("newfeeds.php", update("feedsDiv"));

上面的update函数的原型本来应该是update(id, data),接收两个参数。柯里化之后则先接收id,确定刷新区域是哪里,返回接收余下参数的一个函数作为refresh函数的callback,等到ajax返回data结果之后,在传入callback,更新页面。或许有人会问,为何不直接把refresh设计成refresh(id, url)?这是因为update的方法可能有很多,可能还有update1,update2这样一系列方法,对返回结果的处理都不同,通过这种“柯里化”的方式可以使得refresh得到更高阶的抽象,更好的重用。

 

五、Memoization递归优化

递归是拖慢脚本运行速度的大敌之一,太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出。有了闭包之后,我们就可以通过memoization技术来替代函数中太多的递归调用,提升JavaScript效率。

Memoization说白了,就是在函数中缓存下之前的运行结果,这样我们就不需要重新计算那些已经计算过的结果了。

举个熟悉的例子——斐波那契数列(兔子问题)

兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔都不死,那么一年以后可以繁殖多少对兔子?

这是个烦人的小学问题,答案我就不和大家纠结了——第n月的兔子数量为F(n),F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)(n>=2,n∈N*)。

写成javascript代码如下:

function fibonacci(n){
    return n<2 ? n : fibonacci(n-1) + fibonacci(n-2);
};

恩,看起来短小精悍,蛮顺眼的。不过如果你敢试试fibonacci(100)的话……那你的浏览器就“自挂东南枝”了……

为啥会这样子捏?我们给它加个计数器就知道了(节省时间就不演示了),我们调用fibonacci(10)的时候,它自己调用自己176次,总计177次,11的时候287次,12的时候465次……fibonacci(20)的时候21891次……这也太坑爹了!

其实fabonacci调用自身的时候,有很多计算结果都是已经得出的,太多无用的重复计算,最终使得调用栈溢出了。咋办捏?

不知你还记不记得,现在我们可以在函数内保存对象了!这就是我要说的Memoization方法。上代码:

//接收两个参数, 递归方法, 缓存对象
function memoizer(fun, cache) {
    cache = cache || {};
    //定义递归函数的壳,具体怎么递归是fun说的算
    var shell = function(arg) {
        //如果不在缓存中, 递归计算并放入缓存
        //否则直接返回缓存的结果
        if (! (arg in cache)) {
            //需要在fun中定义shell的递归的方法
            cache[arg] = fun(shell, arg);
        }
        return cache[arg];
    };
    return shell;
}

这是一个通用的方法,此时我们可以这样改写fabonacci:

var factorial = memoizer(function(shell, n){
    return n<2 ? n : shell(n-1) + shell(n-2);
});

这回,fabonacci(100)是淡定无压力呀——354224848179262000000,只调用了101次 [大笑] ,自豪感油然而生……

同理,什么阶乘运算之类的都可以用Memoization的方法解决,so easy!

 

六、函数式代码风格

在一些语言中,连续运算被认为是不良好的编程习惯。我们被要求运算出一个结果值,先放到中间变量中,然后拿中间变量继续参与运算。然而在函数式的语言中,连续运算是被推崇的方法(原因未明,个人没接触过纯函数式语言,理解不深)

在 JavaScr ipt 中,一种常见的情况就是连续赋值:

var a = b = c = d = 100;

还有我们常用的“短路”条件:||和&&(||用来提供变量的默认值,&&可避免从undefined中取值抛出异常),也是连续运算的体现。

再例如,三元表达式:

var objType = getFromInput();
var cls = ((objType == 'String') ? String :
    (objType == 'Array') ? Array :
    (objType == 'Number') ? Number :
    (objType == 'Boolean') ? Boolean :
    (objType == 'RegExp') ? RegExp :
    Object
);
var obj = new cls();

如果你要用if/else,switch,甚至于“多态”的手段去重写上面的方法,绝对是长长一坨……个人感觉这种代码风格还是很容易懂的,不见得比if/else差。

当然,不得不说的还有JS攻城师最喜欢的“链式调用”,例如jQuery的DOM操作:

//举个jQuery的栗子~
$('#item').width('100px')
        .height('100px').
        .css('padding','20px')
        .click(function(){
            alert('hello');
         });

其实原理就是在函数最后return this,即可接着之前的上下文环境继续调用函数,这样很爽吧!

 

七、函数内的运算对函数外无副作用

其实,这并不是JavaScript的一个特性,这是函数式语言应当达到的一种特性,在 JavaScr ipt 中这项特性只能通过开发人员的编程习惯来保证。

所谓对函数外无副作用,含义在于:

  • 函数使用入口参数进行运算,而不修改它(作为值参数而不是变量参数使用)
  • 在运算过程中不会修改函数外部的其它数据的值(例如全部变量)
  • 运算结束后通过函数返回向外部系统传值。

这样有啥好处呢?

没有函数修改过在其作用域之外的量并被其他函数使用(如类成员或全局变量)——这意味着函数求值的结果只是其返回值,而惟一影响其返回值的就是函数的参数。

如果一个函数式程序不如你期望地运行,调试是轻而易举的。因为函数式程序的 bug 不依赖于执行前与其无关的代码路径,你遇到的问题就总是可以再现。在单元测试中,你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态。所有要做的就是传递代表了边际情况的参数。如果程序中的每个函数都通过了单元测试,你就对这个软件的质量有了相当的自信。

而命令式编程就不能这样乐观了,在 Java 或 C++ 中只检查函数的返回值还不够——我们还必须验证这个函数可能修改了的外部状态。

这种特性其实也是程序“高内聚,低耦合”的一种体现,在实际开发中应当尽量遵从。

 

发布者

Rolf

伪文艺IT攻城师,热爱前端,热爱互联。

《JavaScript小特性(6)——函数式编程》有6个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

[喜欢] [嘻嘻] [奋斗] [问号] [鼓掌] [泪] [酷] [强] [耶] [握手] [心] [给力] [神马] [围观] [奥特曼] more »