一、 面向过程编程、面向对象编程、函数式编程概要
1.命令式编程:即过程式编程。强调怎么做。
2.面向对象编程: 通过对一类事物的抽象,即class,其中对象是基本单元。常用的是继承方式。 平时会看到生命周期、链式调用。比如react中的类组件。
3.函数式编程:即声明式编程。强调做什么。更加符合自然语言。常用的是组合的方式。平时看到的数据驱动(响应式编程)。比如react的函数组件+hooks。
二、函数式编程特性
1.纯函数:相同的输入,永远会得到相同的输出。即也就是数学函数。
具体理解两点: 没有副作用(数据不可变): 不修改全局变量,不修改入参。最常见的副作用就是随意操纵外部变量,由于js对象是引用类型。不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。如下:
// 非纯函数 const curUser = { name: 'Peter' } const saySth = str => curUser.name + ': ' + str; // 引用了全局变量 const changeName = (obj, name) => obj.name = name; // 修改了输入参数 changeName(curUser, 'Jay'); // { name: 'Jay' } saySth('hello!'); // Jay: hello! // 纯函数 const curUser = { name: 'Peter' } const saySth = (user, str) => user.name + ': ' + str; // 不依赖外部变量 const changeName = (user, name) => ({...user, name }); // 未修改外部变量 const newUser = changeName(curUser, 'Jay'); // { name: 'Jay' } saySth(curUser, 'hello!'); // Peter: hello! |
2.通过事例进一步加深对纯函数的理解
let arr = [1,2,3]; arr.slice(0,3); //是纯函数 arr.splice(0,3); //不是纯函数,对外有影响 function add(x,y){ // 是纯函数 return x + y // 无状态,无副作用,无关时序,幂等 } // 输入参数确定,输出结果是唯一确定 let count = 0; //不是纯函数 function addCount(){ //输出不确定 count++ // 有副作用 } function random(min,max){ // 不是纯函数 return Math.floor(Math.radom() * ( max - min)) + min // 输出不确定 } // 但注意它没有副作用 function setColor(el,color){ //不是纯函数 el.style.color = color ; //直接操作了DOM,对外有副作用 } |
3.强调使用纯函数的意义是什么,也就是说函数式编程的特性是什么。
更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
自文档化:由于纯函数没有副作用,所以其依赖很明确,因此更易于观察和理解。配合类型签名(一种注释)更清晰。
便于测试和优化: 相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
4.在实际的react开发中,也会看到纯函数的应用。
三、函数式编程理解
1.数学函数和范畴学
数学函数:在数学上,学习一次函数、二次函数等一个特点就是输入x通过函数都会返回有且只有一个输出值y。这里的函数充当映射关系的桥梁。这也正是函数式编程要求必须是纯的原因
范畴:包括值 和 值的变形关系(函数)。即范畴好似一个容器包含这两样东西。
2.js中的函数
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
包括 普通函数 和 剪头函数 。剪头函数(简洁同时this指向定义的时候已经确定)也是函数式编程中的一个使用体现。
3.命令式与声明式
前面提到说函数式编程更加符合自然语言,就是因为其用了声明式。举个例子对比一下
// 命令式-----强调怎么做 var makes = []; for (i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 声明式-----做什么 var makes = cars.map(function(car){ return car.make; }); |
4.高阶函数
高阶函数:参数或返回值为函数的函数。比如可以用于拦截和监控,比如防抖和节流的实际开发中的应用(下面的例子是不考虑this的情况)
防抖的例子:在time的时间内,你连续点击几次,会先清除计时器clearTimeout,当你停下来的时候,根据最后一次点击等待time时间执行func
function debounce(func, time) { let timeout = null; return function() { if (timeout) { clearTimeout(timeout) } timeout = setTimeout(() => { timeout = null; func.apply(null, arguments) // 假如不考虑this }, time); } } |
节流的例子:if语句的执行,取决与setTimeout中对flag值的恢复。time时间到了,就执行一次。这个里面setTimeout()和 func(),一个在前,一个在后,但是执行顺序并不是同步执行。这里的setTimeout是异步的。前面的《理解JS异步》有介绍。
function throtle(func, time) { let flag = true; return function () { if (flag) { flag = false; setTimeout(() => { flag = true }, time); func.apply(null, arguments) // 假如不考虑this } } |
所以呢,高阶函数的这两个例子也就是声明式的,可以当做工具函数,我们用到的时候调用这两个函数就行。因此这样的函数的名字起的语义化就由为重要。
5.纯函数
再次提起纯函数:没有副作用(不修改外部变量)+无状态(不依赖外部变量)。具体的跳会前面的介绍查看。
当了解完函数式编程的基本概念和要点后,然而让我们利用函数式编程可能还无法下手。这时候我们用上几个好的方法规范自己编程更加高效。所以下面的6-11可以说成都是编写函数式程序的方法or工具。
6.函数柯里化(curry)
a. 什么是柯里化
柯里化指的是将一个多参数的函数拆分成一系列单参数函数。
// 柯里化之前 function add(x, y) { return x + y; } add(1, 2) // 3 // 柯里化之后----ES5写法 function add(x) { return function (y) { return x + y; }; } add(1)(2) // 3 // 柯里化之后----ES6的写法 const add = x => y => x + y; |
b. 柯里化的应用
eg1:获取数组对象的某个属性用柯里化优化
比如我们有这样一段数据: let person = [{name: 'kevin'}, {name: 'daisy'}]; 如果我们要获取所有的 name 值,我们可以这样做 let name = person.map(function (item) { return item.name; }) 不过如果我们有 curry 函数: let prop = curry(function (key, obj) { return obj[key] }); let name = person.map(prop('name')) 我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些? 但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了。 |
eg2:高级柯里化,可以看看 ramda库中提供的curry,它提供的好像和我们上面提到的概念不一致,原因参数不需要一次只传入一个 & 占位符值R.__ _表示R.__,表示还未传入的参数 。所以可以称作为高级柯里化
const addFourNumbers = (a, b, c, d) => a + b + c + d; const g = R.curry(addFourNumbers); // 每次都单参数也可以 g(1)(2)(3) // 多参数也可以 g(1)(2, 3) g(1, 2)(3) g(1, 2, 3) g(_, 2, 3)(1) g(_, _, 3)(1)(2) g(_, _, 3)(1, 2) g(_, 2)(1)(3) g(_, 2)(1, 3) g(_, 2)(_, 3)(1) |
c. 柯里化的实现
function curry(func) { return function curried(...args) { if (args.length >= func.length) { // 通过函数的length属性,来获取函数的形参个数 return func.apply(this, args); } else { return function (...args2) { return curried.apply(this, args.concat(args2)); }; } } } |
7.偏函数
a.偏函数是什么
偏函数:固定任意元参数,在平时开发中用到的如下:
// 假设一个通用的请求 API const request = (type, url, options) => ... // GET 请求 request('GET', 'http://a....') request('GET', 'http://b....') request('GET', 'http://c....') // POST 请求 request('POST', 'http://....') // 但是通过部分调用后,我们可以抽出特定 type 的 request const get = request('GET'); get('http://', {..}) |
b.柯里化与偏函数的区别:
- 柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
- 偏函数则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
c.偏函数的实现
function partial(fn) { let args = [].slice.call(arguments, 1); return function () { const newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; } |
8.惰性函数
a. 惰性函数是什么
惰性函数解决每次都要进行判断的这个问题,解决办法是重写函数.
eg:我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。
// 普通方法 : 一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断 let t; function foo() { if (t) return t; t = new Date() return t; } // 闭包 : 没有解决调用时都必须进行一次判断的问题 let foo = (function() { let t; return function() { if (t) return t; t = new Date(); return t; } })(); // 函数对象: 依旧没有解决调用时都必须进行一次判断的问题 function foo() { if (foo.t) return foo.t; foo.t = new Date(); return foo.t; } // 惰性函数 : 以上两个存在问题都解决了 (只需要判断一次) let foo = function() { let t = new Date(); // 重写 foo函数 foo = function() { return t; }; return foo(); }; |
b.惰性函数的应用
eg:DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断。
// 普通写法----问题在于我们每当使用一次 addEvent 时都会进行一次判断 function addEvent (type, el, fn) { if (window.addEventListener) { el.addEventListener(type, fn, false); } else if(window.attachEvent){ el.attachEvent('on' + type, fn); } } // 惰性函数写法----判断一次 function addEvent (type, el, fn) { if (window.addEventListener) { addEvent = function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ addEvent = function (type, el, fn) { el.attachEvent('on' + type, fn); } } addEvent(type, el, fn); } // 或者使用闭包-----判断一次 let addEvent = (function(){ if (window.addEventListener) { return function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ return function (type, el, fn) { el.attachEvent('on' + type, fn); } } })(); |
9.函数组合
a.什么是函数组合
函数组合将多个函数合成一个函数,同时也遵循数学上的结合律。
const compose = (f, g) => x => f(g(x)); const f = x => x + 1; const g = x => x * 2; const fg = compose(f, g); fg(1) //3 // 函数组合满足结合律 compose(f, compose(g, t)) = compose(compose(f, g), t) = f(g(t(x))); |
b. 函数组合的应用
eg:我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'
// 使用组合前 let toUpperCase = function(x) { return x.toUpperCase(); }; let hello = function(x) { return 'HELLO, ' + x; }; let greet = function(x){ return hello(toUpperCase(x)); }; greet('kevin'); // 使用组合后----代码从右向左运行 let compose = function(f,g) { return function(x) { return f(g(x)); }; }; let greet = compose(hello, toUpperCase); greet('kevin'); |
eg: 比如将数组最后一个元素大写,假设 log, head,reverse,toUpperCase 函数存在
const upperLastItem = compose(log, toUpperCase, head, reverse); // 也可以如下组合: // 组合方式 1 const last = compose(head, reverse); const shout = compose(log, toUpperCase); const shoutLast = compose(shout, last); // 组合方式 2 const lastUppder = compose(toUpperCase, head, reverse); const logLastUpper = compose(log, lastUppder); |
c. 函数组合的优点
通过上面的例子看出,不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力。
d. 函数组合的实现
// 从右向左结合 const compose = (...fns) => (...args) => fns.reduceRight((val, fn) => fn.apply(null, [].concat(val)), args); |
e.pointfree是什么
pointfree 指的是函数无须提及将要操作的数据是什么样的。pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用
// 非 pointfree,因为提到了数据:name let initials = function (name) { return name.split(' ').map(compose(toUpperCase, head)).join('. '); }; // pointfree let initials = compose(join('. '), map(compose(toUpperCase, head)), split(' ')); initials("hunter stockton thompson"); |
10.函数记忆
a. 函数记忆是什么
函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。
function add(a, b) { return a + b; } // 假设 memorize 可以实现函数记忆 let memorizedAdd = memoize(add); memorizedAdd(1, 2) // 3 memorizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次,这样可以优化性能 |
b. 函数记忆的应用
eg: 以斐波那契数列为例(如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆)
// 未使用前 let count = 0; let fibonacci = function(n){ count++; return n < 2? n : fibonacci(n-1) + fibonacci(n-2); }; for (let i = 0; i <= 10; i++){ fibonacci(i) } console.log(count) // 453 // 使用函数记忆后 let count = 0; let fibonacci = function(n) { count++; return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); }; fibonacci = memorize(fibonacci); for (let i = 0; i <= 10; i++) { fibonacci(i) } console.log(count) // 12 |
c. 函数记忆的实现
function memorize(fn) { const cache = Object.create(null); // 存储缓存数据的对象 return function (...args) { const _args = JSON.stringify(args); return cache[_args] || (cache[_args] = fn.apply(fn, args)); }; }; |
11.函子
Functor函子;Maybe函子;Monad函子;基础概念的理解参考此博文。
最后,更多学习可以结合Ramda库。