继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

JavaScript 函数式编程

守着一只汪
关注TA
已关注
手记 77
粉丝 11
获赞 37

这篇文章将介绍 JavaScript 的函数式编程的理论. 其中有属于语言内置的内容, 其他均为额外实现, 但是所有内容都是类似于 Haskell 的很通用的"纯函数式语言". 首先, 我想先说明一下"纯函数式语言"的含义. 这类语言都是"安全"的, 它们不会产生任何副作用, 例如: 执行表达式不会改变内部状态, 从而导致在下一次调用同一个表达式时产生不同的结果. 这看起来似乎很怪异并且没什么用, 但实际上是有一些列好处的:

  1. 并发. 不会有死锁或者竞争条件, 因为根本不需要锁 - 数据是不可变的. 看起来还是很有前途的吧...

  2. 单元测试. 可以尽情编写单元测试而无需担心状态, 因为根本不存在这个东西. 我们只需要关心测试函数的参数.

  3. 调试. 堆栈信息绝对够用了.

  4. 扎实的理论基础. 函数式语言基于数学形式系统中的λ演算. 这个理论基础使得证明程序的正确性是很直截了当的(例如: 使用归纳法).

希望上述参数足够促使你阅读接下来的部分. 下面, 我会描述 JavaScript 内的函数式要素, 并且提供一些不是原生但是代价很小的实现.

匿名函数

似乎函数式编程变得越来越流行了. 甚至 Java 8 中也拥有了匿名函数(注: 应该是 lambda 表达式), C# 中也已经引入了很长一段时间了. 匿名函数是一种函数定义不会绑定到特定标识符的函数. 如果你不仅仅是编写一些简单的练习而使用 JavaScript, 我相信你应该对于匿名函数是很熟悉的了. 当使用 jQuery 时, 下面代码很可能是你首先会写到的:

$(document).ready(function () {
    //do some stuff});

传递给 $(document).ready 的函数就是一个实实在在的匿名函数. 这个理论提供了巨大的好处, 尤其在我们想保持事物的 DRY ( Don't repeat yourself ) 原则时.

高阶函数

高阶函数是可以接收函数作为参数或者将函数作为返回值的函数. 在 C#, Java 8, Python, Perl, Ruby 等语言中都可以返回并将函数作为参数... 作为最为流行的语言 - JavaScript 已经在很久之前就内置了这些函数式的要素. 下面是一个基本的例子:

function animate(property, duration, endCallback) {
    //Animation here...
    if (typeof endCallback === 'function') {
        endCallback.apply(null);
    }}animate('background-color', 5000, function () {
    console.log('Animation finished');});

上面的代码中存在一个名为 animate 的函数. 它的参数包含: 一个动画元素的属性, 持续时间和一个在动画完成时触发的回调函数. jQuery 中也有这种模式. jQuery 中含有大量的接收函数作为参数的方法, 例如 $.get:

$.get('http://example.com/test.json', function (data) {
    //processing of the data});

JavaScript 内的回调函数处处可见, 它们是完美的异步编程, 例如: 事件处理, ajax 请求, 客户端处理 ( Node.js ) 等等. 正如我上面提到过的, 通过使用回调函数可以避免重复工作. 当你需要基于条件而表现出不同行为的代码时, 回调函数真的是大有裨益的.

还存在另一种高阶函数 - 返回值是函数的函数. JavaScript 同样存在大量非常有效的用例. 比如使用缓存:

/* From Asen Bozhilov's lz library */lz.memo = function (fn) {
    var cache = {};
    return function () {
        var key = [].join.call(arguments, '§') + '§';
        if (key in cache) {
            return cache[key];
        }
        return cache[key] = fn.apply(this, arguments);
    };};

代码中在父函数的局部作用域内定义了 cache 变量. 在每次调用时, 会首先检测是否函数已经被同样的参数调用过, 如果调用过则立即返回, 否则结果会被缓存并返回. 这里甚至有一种暗示: 如果使用相同的参数调用, 则函数会返回同样的结果. 这是一个典型的函数式编程思想.

设想一下这样的情况:

var foo = 1;function bar(baz) {
    return baz + foo;}var cached = lz.memo(bar);cached(1); //2foo += 1;cached(1); //2

有一个名为 bar 的函数, 只接受一个参数 - baz, 返回值为 baz 和全局变量 foo 的和. 当使用 memo 将 bar 函数包装为可缓存的函数并保存一份拷贝的引用给 cached. 首先用参数 1 调用 cached, 函数返回 2. 之后 foo 自增 1 并再次调用 cached. 我们会得到相同的结果, 但却是错误的结果. 那是因为有某种状态存在, 这种状态会在之后的 Monads 部分中被考虑到. 因此我们还是继续把注意力放在高阶函数上, 尤其是返回函数的那些.

闭包

让我们再次看一下 memo. 函数内定义了局部变量 cache 并且返回可缓存的函数. 变量在返回的函数内也是可被访问的, 因为此处创建了一个闭包. 这就是函数式编程的又一个要素, 并且传播的十分广泛. JavaScript 内可以通过闭包实现私有权限:

var timeline = (function () {
    var articles = [];

    function sortArticles() {
        articles.sort(function (a, b) {
            return a.name - b.name;
        });
    }

    return {
        getArticles: function () {
            return articles;
        },
        setArticles: function (articleList) {
           articles = articleList;
           sortArticles();
        }
    };}());

这个例子里有一个名为 timeline 的对象. 它是一个立即执行函数表达式 ( IIFE ) 的执行结果, 返回了一个实为 timeline 的公共接口的对象, 对象中包含属性 getArticles 和 setArticles. 在 IIFE 的词法作用域的内部, 包含了 articles 数组和 sort 函数的定义, 它们通过 timeline 对象是无法直接调用的.

这个主题将会以 ECMAScript 5 的高阶函数作为结束. 近年来, JavaScript 包含了越来越的的函数式要素. 当我们听到函数式编程, 首先浮现在我们脑中的可能就是 map 函数了. 它接收一个匿名函数和一个列表作为参数. 并将列表中的所有元素应用到这个函数中. map 如今已经正式成为 ECMAScript 5 的一部分了.

下面是它的基本用法:

[1,2,3,4].map(function (a) {
    return a * 2;});//[2,4,6,8]

上面的代码中 map 将列表内的元素变为之前的 2 倍. map 并不是唯一被加入到 ECMAScript 5 的典型函数式编程语言的函数, 同样还有 filter 和 reduce.

递归

另一个广泛适用于几乎所有现代编程语言的元素则是递归. 它是一个在函数提内部调用函数本身的一种函数:

function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);}

上面是一个使用递归来实现阶乘的例子. 这个概念应用的十分普遍, 因此就不再赘述了.

状态管理(Monads)

在 memoization 的例子中, JavaScript 不是一个纯粹的函数式语言 (也许这也是这门语言之所以如此流行的一个原因...) , 因为它包含了可变的数据与状态. 通常纯函数式编程语言 (比如: Haskell ) 使用 monads 管理状态. JavaScript 中有多种 Monads 的实现方式. 例如下面这个就是 Douglas Crockford 的实现:

/* Code by Douglas Crockford */function MONAD(modifier) {
    'use strict';
    var prototype = Object.create(null);
    prototype.is_monad = true;
    function unit(value) {
        var monad = Object.create(prototype);
        monad.bind = function (func, args) {
            return func.apply(
                undefined,
                [value].concat(Array.prototype.slice.apply(args || []))
            );
        };
        if (typeof modifier === 'function') {
            modifier(monad, value);
        }
        return monad;
    }
    unit.method = function (name, func) {
        prototype[name] = func;
        return unit;
    };
    unit.lift_value = function (name, func) {
        prototype[name] = function () {
            return this.bind(func, arguments);
        };
        return unit;
    };
    unit.lift = function (name, func) {
        prototype[name] = function () {
            var result = this.bind(func, arguments);
            return result && result.is_monad === true ? result : unit(result);
        };
        return unit;
    };
    return unit;}

这是一个简短的演示, 来说明如何创建一个一元 I/O:

var monad = MONAD();monad(prompt("Enter your name:")).bind(function (name) {
    alert('Hello ' + name + '!');});

在 JavaScript 内使用 monads 看起更像学术练习而并没有什么实际作用, 但是如果想在纯函数式方式下工作, 我们就能用到它了.

Schönfinkelization (柯里化)

Schönfinkelization 是一种函数式的变形, 它使我们可以逐步的填充函数的参数. 当函数接收到最后一个参数时才会返回结果. 它由 Moses Schönfinkel 发明并在之后由 Haskell Curry 重新发现. 下面是一个在 JavaScript 内的样例, 由 Stoyan Stefanov 实现:

/* By Stoyan Stafanov */function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
        stored_args = slice.call(arguments, 1);
    return function () {
        var new_args = slice.call(arguments),
            args = stored_args.concat(new_args);
        return fn.apply(null, args);
    };}

接下来是一个使用函数来解二次方程的例子:

function quadraticEquation(a, b, c) {
    var d = b * b - 4 * a * c,
        x1, x2;
    if (d < 0) throw "No roots in R";
    x1 = (-b - Math.sqrt(d)) / (2 * a);
    x2 = (-b + Math.sqrt(d)) / (2 * a);
    return {
        x1: x1,
        x2: x2    }}

如果需要逐步填充函数的参数则可以:

var temp = schonfinkelize(quadraticEquation, 1);temp = schonfinkelize(temp, -2);temp(1); // { x1: 1, x2: 1 }

如果想使用语言内置功能来取代 schonfinkelize 函数, 你可以使用 Function.prototype.bind . 由于它定义在所有函数的构造函数的原型属性上, 因此可以作为函数的方法进行使用. bind 会创建一个包含特定上下文与参数的全新函数. 例如:

var f = function (a, b, c) {
  console.log(this, arguments);};

接下来, 使用 bind 函数:

var newF = f.bind(this, 1, 2);newF(); //window, [1, 2]newF = newF.bind(this, 3)newF(); //window, [1,2,3]newF(4); //window, [1,2,3,4]

使用 bind 时候需要注意, 它并没有被广泛支持. 基于 kagax ES5 compatibility table , IE9 以上才支持 bind.

这个功能在某些场景下是很实用的. 设想有一个 session 对象. session 的键可以同构复杂耗时的算法生成, 一旦用户访问网站就需要生成. session 对象也应该保存关于用户的一些信息, 例如: 客户选择的当前页面的皮肤. 这样就可以使用柯里化来调用 session 的生成函数, 一旦页面加载, 就会生成 session 的键. 在那之后可以再次调用函数来获取用户皮肤. 这样就会在用户选择完皮肤后才生成 session 对象, 但却不会有额外开销, 因为生成 session 键值的复杂算法已经在之前执行过了(这个过程对用于不敏感).

模式匹配

函数式编程里最酷的一件事就是模式匹配了. 事实上, 我也不确定为什么我这么喜爱它, 或许是因为它的简洁并且可以把问题打散为更小的部分. 例如: 在 Haskell 中, 可以这样定义阶乘算法:

factorial 0 = 1factorial n = n * factorial (n - 1)

看起来很酷吧. 你把一个巨大的任务分割成了一个个更小的部分, 并且将方案变得更加简单. 当我开始写这篇文章的时候, 我已经有了在JavaScript中实现类似功能的想法, 但是后来我发现它已经有了实现方案. 下面就是来自于 Bram Stein 的 funcy 的例子:

var $ = fun.parameter,
    fact = fun(
        [0, function ()  { return 1; }],
        [$, function (n) { return n * fact(n - 1); }]
    );

在 fun 内部有一个特殊的参数变量 - fun.parameter. 当通过空字符串 "" 调用 fact 时, function () { return 1; } 就会被执行, 否则执行 function (n) { return n * fact(n – 1); }, 是不是很酷?

我希望我已经为你们足够清晰的展示了 JavaScript 的所有的函数式的部分并且让大家理解了函数式编程的优美与简洁.

原文链接:http://outofmemory.cn/javascript/javascript-functional-program

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP