JavaScript 函数式编程到底是个啥

GeorginaPar 2年前
   <p>随着大前端时代的到来,在产品开发过程中,前端所占业务比重越来越大、交互越来越重。传统的老夫拿起JQuery就是一把梭应付当下重交互页面已经十分乏力。于是乎有了Angular,React,Vue这些现代框架。</p>    <p>但随之而来的还有大量的新知识新名词,如MVC,MVVM,Flux这些设计模式就弄得很多同学傻傻分不清。这时候又见到别人讨论什么函数式编程,更是一脸懵逼了。</p>    <p>我们大多听过面向对象编程,面向过程编程,那啥又是函数式编程呢?在我们前端开发中又有哪些应用场景?我抱着这个疑惑,初步的学习了下。 <em>(此文仅是学习,无甚干货)。</em></p>    <h2>函数式编程</h2>    <h3>定义</h3>    <p>函数式编程(Functional Programming,后面简称FP),维基百科的定义是:</p>    <p>是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。</p>    <p>我来尝试理解下这个定义,好像就是说,在敲代码的时候,我要把过程逻辑写成函数,定义好输入参数,只关心它的输出结果。而且可以把函数作为输入输出。感觉好像平常写js时,就是这样的嘛!</p>    <h3>特性</h3>    <p>网上FP的定义与特性琳琅满目。各种百科、博客、一些老师的网站上都有大同小异的介绍。为了方便阅读,我列下几个好像比较重要的特性,并附上我的第一眼理解。</p>    <ol>     <li> <p>函数是一等公民。就是说函数可以跟其他变量一样,可以作为其他函数的输入输出。喔,回调函数就是典型应用。</p> </li>     <li> <p>不可变量。就是说,不能用var跟let咯。按这要求,我似乎有点难写代码。</p> </li>     <li> <p>纯函数。就是没有副作用的函数。这个好理解,就是不修改函数外部的变量。</p> </li>     <li> <p>引用透明。这个也好理解,就是说同样的输入,必定是同样的输出。函数内部不依赖外部状态,如一些全局变量。</p> </li>     <li> <p>惰性计算。大意就是:一个表达式绑定的变量,不是声明的时候就计算出来,而是真正用到它的时候才去计算。</p> </li>    </ol>    <p>还有一些衍生的特性,如柯里化与组合,三言两语说不清,就不阐述了,有兴趣的同学可以自己再了解了解。</p>    <h2>FP在JavaScript中的应用</h2>    <p>React就是典型的FP。它不同于Vue这样的MVVM框架,它仅仅是个View层。</p>    <p>ReactView = render(data) 它只关心你的输入,最终给你返回相应视图。所以你休想在react组件中去修改父组件的状态,更没有与dom的双向绑定。</p>    <p>这个是框架上的应用,那么在我们平常书写JavaScript时有哪些应用呢?换句话说,平常书写js时候,遇到什么情况,我们采用FP会更好。</p>    <p>从最常见的入手吧,如典型的操作数组:</p>    <pre>  <code class="language-javascript">// 从users中筛选出年龄大于15岁的人的名字  const users = [    {      age: 10,      name: '张三',    }, {      age: 20,      name: '李四'    }, {      age: 30,      name: '王五'    }  ];    // 过程式  const names = [];  for (let i = 0; i < users.length; i++)    {    if (users[i].age > 15) {      names.push(users[i].name);    }  }  // 函数式  const names = users.filter(u => u.age > 15).map(u => u.name);</code></pre>    <p>嗯,代码精简了很多,但是貌似带来了更大的开销。如果是非常大的数据,非常多的筛选工作,那就会循环多次。</p>    <p>这里得想到刚刚的惰性计算。按照惰性求值的要求,应该是要最后返回结果时,才真正去筛选年纪并得到姓名数组。</p>    <p>然而JavaScript的数组并不支持惰性求值。这时候我们得上一些工具库,如 <a href="/misc/goto?guid=4958871496205746239" rel="nofollow,noindex">Lodash</a> 。可以看下它文档中的例子: <a href="/misc/goto?guid=4959750049680312037" rel="nofollow,noindex">_.chain</a> 。</p>    <p>好像也没好到哪里去啊,不就是把多行代码变一行嘛?说的那么玄乎,还多了性能开销,然后又跟我说得上个工具库。。。</p>    <p>说的好像很有道理,但是for循环是有个弊端的,它产生了变量i,而这个变量又是不可控的,如果业务逻辑一复杂,谁知道它循环到什么时候i有没有发生变化,然后导致循环出问题呢?</p>    <p>我们再看一个与DOM交互的场景:</p>    <p>假如页面有一个按钮 button ,我们需要求出用户点击了几次,但是一秒钟内重复点击的不算。传统方法会这么写。</p>    <pre>  <code class="language-javascript">var count = 0;  var rate = 1000;  var lastClick = Date.now() - rate;  var button = document.querySelector('button');  button.addEventListener('click', () => {    if (Date.now() - lastClick >= rate) {      console.log(`Clicked ${++count} times`);      lastClick = Date.now();    }  });</code></pre>    <p>妥,完全没问题。但是发现多了很多状态,count,rate,lastClick,还得对比来对比去。那如果用FP会是怎么样的呢?</p>    <p>抱歉。。。没法写。。。除非很强大的编程能力,自己封装好方法去处理。所以在这里,我们可以上个工具--- <a href="/misc/goto?guid=4959717416649186477" rel="nofollow,noindex">Rx.js</a> ,上述的例子就是rxjs中引用的,我们看它是如何优雅地处理的。</p>    <pre>  <code class="language-javascript">var button = document.querySelector('button');  Rx.Observable.fromEvent(button, 'click')    .throttleTime(1000) // 每隔1000毫秒才能触发事件    .scan(count => count + 1, 0) // 求值,默认值是0    .subscribe(count => console.log(`Clicked ${count} times`)); // 订阅结果、输出值</code></pre>    <p>巧夺天工!再也不用去管理状态了,不需要声明一堆变量,修改来修改去,判断来判断去,简直完美。</p>    <p>平常我们有很多需要更新dom的异步操作,如搜索行为:用户连续输入查询值,如果停顿半秒就执行搜索,如果搜索了多次,发起了多次请求,那只返回最终输入的那次搜索结果。</p>    <p>闭上眼想想,你之前是怎么实现的。反正我都是设置开始时间,结束时间,上次时间,等等变量。繁琐,而且不可控。</p>    <p>当我们以FP的思想去实现时,就会想方设法的减少变量,来优雅程序。最常见的方法就是用下别人的工具库来实现它。当然有些简单的场景也可以自己实现,最主要的还是要有这个意识。</p>    <p>其实我们平常已经写了一些FP了,只是我们没意识到,或者没怎么写好。就好比闭包,很多人都不了解闭包的概念,但实际上已经写了很多闭包代码。其实闭包本身也是函数式编程的一个应用。</p>    <p>鉴于我自己理解也不深,没法多阐述FP的应用,大家如果有兴趣,可以多了解了解。</p>    <h2>FP在JavaScript中的优劣势</h2>    <p>总结一下FP的优劣,以便于我们在实际开发中,能更好的抉择是否采用FP。</p>    <h3>优势</h3>    <ol>     <li> <p>更好的管理状态。因为它的宗旨是无状态,或者说更少的状态。而平常DOM的开发中,因为DOM的视觉呈现依托于状态变化,所以不可避免的产生了非常多的状态,而且不同组件可能还相互依赖。以FP来编程,能最大化的减少这些未知、优化代码、减少出错情况。</p> </li>     <li> <p>更简单的复用。极端的FP代码应该是每一行代码都是一个函数,当然我们不需要这么极端。我们尽量的把过程逻辑以更纯的函数来实现,固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响。</p> </li>     <li> <p>更优雅的组合。往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。参考上面第二点,更强的复用性,带来更强大的组合性。</p> </li>     <li> <p>隐性好处。减少代码量,提高维护性。</p> </li>    </ol>    <h3>劣势</h3>    <ol>     <li> <p>JavaScript不能算是严格意义上的函数式语言,很多函数式编程的特性并没有。比如上文说的数组的惰性链求值。为了实现它就得上工具库,或者自己封装实现,提高了代码编写成本。</p> </li>     <li> <p>跟过程式相比,它并没有提高性能。有些地方,如果强制用FP去写,由于没有中间变量,还可能会降低性能。</p> </li>     <li> <p>代码不易读。这个因人而异,因码而已。特别熟悉FP的人可能会觉得这段代码一目了然。而不熟悉的人,遇到写的晦涩的代码,看着一堆堆lambda演算跟匿名函数 () => () => () 瞬间就懵逼了。看懂代码,得脑子里先演算半小时。</p> </li>     <li> <p>学习成本高。一方面继承于上一点。另一方面,很多前端coder,就是因为相对不喜欢一些底层的抽象的编程语言,才来踏入前端坑,你现在又让他们一头扎入FP,显得手足无措。</p> </li>    </ol>    <h2>总结</h2>    <p>个人觉得,FP还是好的。对于开发而言,确确实实能优化我们的代码,熟悉之后,也能提高编程效率。对于编程本身而言,也能拓展我们的思维,不局限在过程式的编程代码。</p>    <p>在编写JS中,可以尽量的运用FP的思维,如不可变量、纯函数、惰性求值。但也不必教条式的遵循函数式编程,一定要怎样怎样。比如我们看下知乎大V某温的一个回答: <a href="/misc/goto?guid=4959750049835650937" rel="nofollow,noindex">传送门</a> 。</p>    <p>唉,做个页面仔不容易啊。但是不想当大牛的页面仔不是好页面仔!</p>    <h2>参考</h2>    <ol>     <li> <p><a href="/misc/goto?guid=4959750049947382390" rel="nofollow,noindex">函数式编程入门教程-阮一峰</a></p> </li>     <li> <p><a href="/misc/goto?guid=4958979803052981409" rel="nofollow,noindex">函数编程语言-维基百科</a></p> </li>     <li> <p><a href="/misc/goto?guid=4959750050102599204" rel="nofollow,noindex">前端开发js函数式编程真实用途体现在哪里?-知乎答者</a></p> </li>    </ol>    <p> </p>    <p>来自:https://segmentfault.com/a/1190000009864459</p>    <p> </p>