函数式编程就是通过纯函数以及它们的组合、柯里化、Functor等技术来降低系统复杂度。
一、声明式和命令式
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
1、声明式
程序抽象了控制流过程,花费大量代码描述的是数据流:即做什么。代码更多依赖表达式,表达式通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。
2、命令式
程序花费大量代码来描述用来达成期望结果的特定步骤,控制流:即如何做。代码中频繁使用语句,通用的语句包括for、if、switch、throw,等等。
函数式编程是一个声明式范式,意思是说程序逻辑不需要通过明确描述控制流程来表达。
二、纯函数
给它同样的输入,它总是返回同样的结果,并且没有副作用。
函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。在函数式编程中,函数就是一个管道(pipe),这头进去一个值,那头就会出来一个新的值,没有其他作用。因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
函数式编程中的函数不是指计算机中的函数,而是指数学中的函数,即自变量的映射。也就是说一个函数的值仅决定于函数参数的值,不依赖其他状态。比如sqrt(x)函数计算x的平方根,只要x不变,不论什么时候调用,调用几次,值都是不变的。
三、函数复合
函数复合是结合两个或多个函数,从而产生一个新函数或进行某些计算的过程。
高阶函数指的是一个函数以函数为参数,或以函数为返回值,或者既以函数为参数又以函数为返回值。
1、函数合成
f(x)和g(x)合成为f(g(x)),前提是f和g都只能接受一个参数。如果可以接受多个参数,比如f(x, y)和g(a, b, c),函数合成就非常麻烦,这时需要函数柯里化。
2、回调函数
函数f有一个参数,这个参数是函数g,当函数f执行完以后执行函数g,这个过程就叫回调。函数g是以参数形式传给函数f的,那么函数g就叫回调函数。如果g是一个匿名函数,则为匿名回调函数。
3、柯里化
4、函子
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。
Maybe、Either、Monad这三种强大的Functor,在链式调用、惰性求值、错误捕获、输入输出中都发挥着巨大的作用。
任何具有map方法的数据结构,都可以当作函子的实现。一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
(new Functor(2)).map(function (two) { return two + 2; }); // Functor(4)
上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。
函数式编程一般约定,函子有一个of方法,用来生成新的容器。
Functor.of = function(val) { return new Functor(val); }; Functor.of(2).map(function (two) { return two + 2; }); // Functor(4)
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。
A、Maybe函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
Functor.of(null).map(function (s) { return s.toUpperCase(); }); // TypeError
Maybe函子的主要应用,就是设置空值检查。
class Maybe extends Functor { map(f) { return this.val ? Maybe.of(f(this.val)) : Maybe.of(null); } }
B、Either函子
Either函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
class Either extends Functor { constructor(left, right) { this.left = left; this.right = right; } map(f) { return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right); } }
Either.of = function (left, right) { return new Either(left, right); };
/** * 应用场景:条件运算 */ var addOne = function (x) { return x + 1; }; Either.of(5, 6).map(addOne); // Either(5, 7); Either.of(1, null).map(addOne); // Either(2, null);
/** * 应用场景:提供默认值 */ Either .of({name: 'CamilleHou'}, currentUser.name) .map(updateField);
/** * 应用场景:代替try catch */ function parseJSON(json) { try { return Either.of(null, JSON.parse(json)); } catch (e: Error) { return Either.of(e, null); } }
C、Monad函子
Maybe.of( Maybe.of( Maybe.of({name: 'CamilleHou', age: 2020}) ) )
上面这个函子,一共有三个Maybe嵌套。如果要取出内部的值,就要连续取3次this.val。这当然很不方便,因此就出现了Monad函子。Monad函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
class Monad extends Functor { join() { return this.val; } flatMap(f) { return this.map(f).join(); } }
Monad函子的重要应用,就是实现I/O(输入输出)操作。
5、常用函数
filter,map,reduce。
6、debounce函数
debounce函数,又称"去抖函数"。即防止某一函数被连续调用,从而导致浏览器卡死或崩溃,等待一段时间再调用。
var myFunc = debounce(function(){ //繁重、耗性能的操作 },250); window.addEventListener('resize',myFunc);
function debounce(fn,wait){ var timer; return function(){ clearTimeout(timer); timer= setTimeout(fn,wait); } }
function debounce(fn, delay){ var timer = null; // 声明计时器 return function(){ var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function(){ fn.apply(context, args); }, delay); }; }
// 当用户滚动时函数会被调用 function foo() { console.log('You are scrolling!'); } // 在事件触发的两秒后,我们包裹在debounce中的函数才会被触发 let elem = document.getElementById('container'); elem.addEventListener('scroll', debounce(foo, 2000));
7、throttle函数,节流
像窗口的resize,这类能够以较高的速率触发的事件,非常适合用"去抖函数",这时也可称作"函数节流",固定时间内只能调用一次,避免给浏览器带来过大的性能负担。
具体的实现是:当函数被调用时,不立即执行相应的语句,而是等待固定的时间w,若在w时间内,即等待还未结束时,函数又被调用了一次,则再等待w时间,重复上述过程,直到最后一次被调用后的w时间内该函数都没有被再调用,则执行相应的代码。
/** * 节流 * throttle函数用于控制事件触发频率,requestAnimationFrame方法保证每次页面重绘(每秒60次), * 只会触发一次scroll事件的监听函数。下面方法将scroll事件的触发频率,限制在每秒60次。 */ (function() { var throttle = function(type, name, obj) { var obj = obj || window; var running = false; var func = function() { if (running) { return; } running = true; requestAnimationFrame(function() { obj.dispatchEvent(new CustomEvent(name)); running = false; }); }; obj.addEventListener(type, func); }; // 将scroll事件重定义为optimizedScroll事件 throttle('scroll', 'optimizedScroll'); })(); window.addEventListener('optimizedScroll', function() { console.log("Resource conscious scroll callback!"); });
改用setTimeout方法,可以放置更大的时间间隔。
(function() { window.addEventListener('scroll', scrollThrottler, false); var scrollTimeout; function scrollThrottler() { if (!scrollTimeout) { scrollTimeout = setTimeout(function() { scrollTimeout = null; actualScrollHandler(); }, 66); } } function actualScrollHandler() { // ... } }());
四、共享状态
1、共享状态的情景
任意变量、对象、内存空间存在于共享作用域(全局作用域和闭包作用域)下;
对象属性在各个作用域之间被传递。
2、共享状态存在的问题
A、同步竞争
假如前端开发张三有一个user对象需要保存,他的saveUser()函数向服务器发起一个请求。此时,用户李四改变了自己的头像,通过updateAvatar()触发了另一次 saveUser()请求。在保存动作执行后,服务器返回一个更新的user对象,客户端要将这个对象替换内存中的对象,以保持与服务器同步。
不幸地是,用户李四的请求有可能比前端开发张三的请求更早返回,所以当第一次请求(前端开发张三)返回时,新的头像在内存中丢失了,被替换回旧的头像,等于没有更新成功。
B、改变函数调用次序可能导致一连串的错误。
函数式编程依赖于不可变数据结构和纯粹的计算过程来从已存在的数据中派生出新的数据,避免了共享状态。
函数式编程使用参数保存状态,最好的例子就是递归,即调用自身,尾递归就是在return的地方执行递归。
递归是函数调用自己,迭代是循环调用别的函数。
五、不可变性
不可变对象完全不能被改变,属性也不能动。
函数式编程只是返回新的值,不修改系统变量,避免了易变的数据。
六、副作用
改变了任何外部变量或对象属性;
发网络请求;
写日志。
函数式编程避免了副作用。