JavaScript-可维护代码编写,函数式编程与纯函数

JavaScript-可维护代码编写,函数式编程与纯函数

JavaScript是函数式编程与面向对象编程的混合编程语言,加上本身一些可扩展性(比如:函数参数个数及类型的不确定),使得JavaScript非常灵活,当然也可以说非常不可控。正是这个特点,使得一个团队维护一个共同的前端项目时,JavaScript代码可能非常难以读懂。试想,你新加入一个团队,让你去读别人的代码本身就不太容易,如果团队的注释习惯及编程风格不太好,真的很难读懂每个js文件到底在干什么。因此,编写可维护的高质量代码真的非常重要

1. 问题来源

阅读代码比阅读文章困难是因为,一段代码可能会依赖很多函数,你为了了解每个函数的作用不得不跳到其他代码块,这不符合我们的阅读习惯。 偶尔在一本书中看到这样一句话:“面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩…以及整个丛林”。所以,尝试在我们的代码中更巧妙利用JavaScript的特点,使用函数式编程,可以使得我们的JS代码可维护性更高,可读性更强。

2. 使用纯函数来提高JS代码可维护性

首先我们来了解一下什么是函数式编程。

在函数式编程语言中,函数是第一类的对象,也就是说,函数 不依赖于任何其他的对象而可以独立存在,而在面向对象的语言中,函数 ( 方法 ) 是依附于对象的,属于对象的一部分。这一点就 决定了函数在函数式语言中的一些特别的性质,比如作为传出 / 传入参数,作为一个普通的变量等。

我们常听到这样一种说法,在JavaScript中函数是“一等公民”。当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量…等等。而函数式编程的一个基础就是纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

2.1 理解纯函数

实际上我们可以这样理解,纯函数就是这样一种函数,它不依赖于外部环境(例如:全局变量、DOM)、不改变外部环境(例如:发送请求、改变DOM结构),函数的输出完全由函数的输入决定。
比如 slice 和 splice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。

相关代码请查看github

        var array1 = [0,1,2,3,4,5,6];
        var array2 = [0,1,2,3,4,5,6];

        var spliceArray = array1.splice(0,2);
        var sliceArray = array2.slice(0,2);

        console.log('array1: ' + array1);
        console.log('spliceArray: ' + spliceArray);

        console.log('array2: ' + array2);
        console.log('sliceArray: ' + sliceArray); 

运行结果

array1: 2,3,4,5,6
spliceArray: 0,1
array2: 0,1,2,3,4,5,6
sliceArray: 0,1

可以看到,splice改变了原始数组,而slice没有。我们认为,slice不改变原来数组的方式更加“安全”。改变原始组数,是一种“副作用”。

2.2 非纯函数可能带来的“副作用”

让我们来仔细研究一下“副作用”以便加深理解。那么,我们在纯函数定义中提到的万分邪恶的副作用到底是什么?

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用可能包含,但不限于:

  • 列表内容
  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
    获取用户输入
  • DOM 查询
  • 访问系统状态
    这个列表还可以继续写下去。概括来讲,只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。
        function getName(obj){
            return obj.name;
        }
        function getAge(obj){
            return obj.age;
        }
        function selfIntroduction(people){
            console.log(getName(people));
            console.log(getAge(people));
        }

        var Lee = {
            name: 'LYY',
            age: 25
        };

        selfIntroduction(Lee);

运行结果

LYY
25

显然selfIntroduction这个函数不是纯函数,它依赖于getNamegetAge两个函数,如果我不小心改变了其中某个函数的功能,这将使得selfIntroduction这个函数出现错误。你现在可能感觉自己不会犯这样的错误,但当网页变得复杂,且由多人维护的时候,这将是个很难发现的bug。

2.3 纯函数编程的优点

  1. 可缓存性(Cacheable)
    纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:
        var memoize = function(f) {
            var cache = {};

            return function() {
                var arg_str = JSON.stringify(arguments);
                cache[arg_str] = cache[arg_str] ? cache[arg_str] + '(from cache)' : f.apply(f, arguments);
                return cache[arg_str];
            };
        };

        var squareNumber  = memoize(function(x){ return x*x; });

        console.log(squareNumber(4));       
        console.log(squareNumber(4)); 
        console.log(squareNumber(5));
        console.log(squareNumber(5)); 

执行结果:

16
16(from cache)
25
25(from cache)
  1. 可移植(Portable)
    纯函数是完全自给自足的,它需要的所有东西都能轻易获得。仔细思考思考这一点…这种自给自足的好处是什么呢?首先,纯函数的依赖很明确,因此更易于观察和理解——没有偷偷摸摸的小动作。这使得你在阅读这种代码的时候更容易,一个函数完成一个功能,不再依赖其他函数或者变量。
  2. 可测试(Testable)
    第三点,纯函数让测试更加容易。因为只要每次输入相同,纯函数将输出相同的结果,不需要多次测试同一个输入。
  3. 合理性(Reasonable)
    很多人相信使用纯函数最大的好处是引用透明性(referential transparency)。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
    由于纯函数总是能够根据相同的输入返回相同的输出,所以它们就能够保证总是返回同一个结果,这也就保证了引用透明性。
  4. 并行代码(Parallel)
    最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。
    并行代码在服务端 js 环境以及使用了 web workers 的浏览器那里是非常容易实现的,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。

3. 不要滥用函数式编程或者纯函数

以上分析提到了纯函数的很多优点,但是,这并不是要求我们编写的每一个函数都是纯函数。函数越“纯”,对环境依赖越少,往往意味着要输入更多参数。

3.1 可以提纯的情况举例

var pureHttpCall = memoize(function(url, params){
  return function() { return $.getJSON(url, params); }
});

这里有趣的地方在于我们并没有真正发送 http 请求——只是返回了一个函数,当调用它的时候才会发请求。这个函数之所以有资格成为纯函数,是因为它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。这种技巧结合 柯里化 及 代码组合会使我们的JS代码清晰、可维护。

柯里化 我写过一篇博客,你可以点击阅读。 代码组合其实比较简单,现在举例如下。

        var compose = function(f, g) {
            return function(x) {
                return f(g(x));
            };
        };

        function getPeople(){
            return {};
        }

        function namePeople(p){
            p.name = 'Lee';
            return p;
        }

        var definePeople = compose(namePeople, getPeople);
        var people = definePeople();
        console.log(people);

执行结果:
Object {name: “Lee”}
看,代码组合就是字面意思。

3.2 滥用函数式编程的举例

var getServerStuff = function(callback){
  return ajaxCall(function(json){
    return callback(json);
  });
};
//如果你仔细分析这段代码,它就等价于
var getServerStuff = ajaxCall;

分析过程:

// 这行
return ajaxCall(function(json){
  return callback(json);
});

// 等价于这行
return ajaxCall(callback);

// 那么,重构下 getServerStuff
var getServerStuff = function(callback){
  return ajaxCall(callback);
};

// ...就等于
var getServerStuff = ajaxCall;

还有这种控制器:

var BlogController = (function() {
  var index = function(posts) {
    return Views.index(posts);
  };

  var show = function(post) {
    return Views.show(post);
  };

  var create = function(attrs) {
    return Db.create(attrs);
  };

  var update = function(post, attrs) {
    return Db.update(post, attrs);
  };

  var destroy = function(post) {
    return Db.destroy(post);
  };

  return {index: index, show: show, create: create, update: update, destroy: destroy};
})();

我们可以直接把它重写成:

var BlogController = {index: Views.index, show: Views.show, create: Db.create, update: Db.update, destroy: Db.destroy};

当你滥用函数式编程时,很可能使得你的代码很难懂。所以,我的建议是,代码保持最直接简洁的状态,尽量使用纯函数(容易维护)、适当情况下使用函数式编程(容易看懂)

你可能感兴趣的:(JavaScript,函数式编程,纯函数,高级编程技巧)