函数式编程
概念:函数式编程是一种将电脑运算视为函数运算的编程范式(程序设计)。
// 伪代码
// 函数式编程
function fb (n) {
if ( n <= 1 ) {return 1};
return fb(n - 1) + fb(n - 2);
}
// 指令式
function fb (n) {
let arr = [1,1]
for(let i=2;i<n;i++){
arr[i] = arr[i-1] + arr[i-2]
}
return arr[n-1]
}
要求:在函数式编程中,函数是一等公民。所谓一等公民,其实就是普通公民,其他数据类型一样,没有什么特殊的,我们可以像对待任何其他数据类型一样对待函数。
function delay() {
console.log('5000ms 后执行 delay 方法!')
}
setTimeout(() => {
delay()
}, 5000)
setTimeout(delay, 5000)
思想:强调程序执行的结果而非执行的过程
我们在使用函数时,只需要关心该函数实现了什么功能,需要什么参数,而不需要过多的关心函数的内部实现,就能够顺利的将函数运用起来。我们使用的各种工具都是同样的思想,只需要知道函数的功能和参数即可。
纯函数
定义:相同的输入总会得到相同的输出,并且不会产生副作用的函数。
可靠性:
因为没有副作用,所以可靠。使用纯函数不必考虑任何的数据变化。
let source = [1, 2, 3, 4, 5];
source.slice(1, 3); // 纯函数 返回[2, 3] source不变
source.splice(1, 3); // 不纯的 返回[2, 3, 4] source被改变
source.pop(); // 不纯的
source.push(6); // 不纯的
source.shift(); // 不纯的
source.unshift(1); // 不纯的
source.reverse(); // 不纯的
source = [1, 2, 3, 4, 5];
function getLast(arr) {
return arr[arr.length-1];
}
function getLast_(arr) {
return arr.pop();
}
console.log(getLast(source),source);
console.log(getLast_(source),source);
可移植性:
我们的utils文件里的方法绝大多数都是纯函数,可以一次封装多次使用。例如getParams(url, param)输出结果只和参数有关且无副作用。
可缓存性:
因为相同的输入总能得到相同的输出,因此,如果函数内部计算非常复杂,当我们发现输入与上一次相同时,可以直接返回结果而不用经过内部的计算。这是一种性能优化的策略。
// 传入日期,获取当天的数据
function process(date) {
var result = '';
// 假设这中间经历了复杂的处理过程
return result;
}
function withProcess(base) {
var cache = {}
return function() {
var date = arguments[0];
if (cache[date]) {
return cache[date];
}
cache[date] = base.apply(base, arguments);
return cache[date]
}
}
var _process = withProcess(process);
// 经过上面一句代码处理之后,我们就可以使用_process来获取我们想要的数据,
// 如果数据存在,会返回缓存中的数据,
// 如果不存在,则会调用process方法重新获取。
_process('2017-06-03');
_process('2017-06-04');
_process('2017-06-05');
高阶函数
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
定义:以函数作为参数或者返回值的函数
函数作为参数
数组map:
Array.prototype.myMap = function(fn,context){
let tem = []
if(typeof fn=='function'){
let k = 0
const len = this.length
while(k<length){
tem.push(fn.call(context,this[k],k))
k++
}
return temp
}
}
由此可见,map方法中的循环过程是公共逻辑,而具体对每一项做什么则由fn参数传入,来让使用者自定义。我们可将传入的函数成为基础函数,而map方法就可以称为高阶函数。类似:数组filter、promise里的回调
函数作为返回值
// 根据参数type返回不同的类型判断函数
function isType(type){
return function (obj){
return Object.prototype.toString.call( obj ) === '[object '+type+']';
}
};
h5组件库中的createNamespace
混合应用
// 单例模式
var getInstance = function(fn) {
var result;
return function(){
return result || (result = fn.call(this,arguments));
}
};
//防抖
function debounce(fn,delay=1000){
let timeout = null
return function(){
clearTimeout(timeout)
timeout = setTimeout(()=>{
fn.apply(this,arguments)
},delay)
}
}
// 节流
function throttle(fn,delay=1000){
let run = true
return function(){
if (!canRun) return;
canRun = false;
setTimeout(()=>{
fn.apply(this,arguments)
canRun = true
},delay)
}
}
这里高阶函数的优势:1、封装公共逻辑;2、动态定义;3、在需要执行时调用
多熟悉一些成熟的应用场景并尝试在代码中使用,以便写出结构更合理、逻辑更清晰的代码。
函数柯里化
柯里化是高阶函数的一种用法。
定义:是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数的新函数的技术。 (一个函数(比如叫curry)接受一个函数A为参数,返回一个新函数curA,A(m,n) = curA(m)(n))
常见的一道题:
function add(a, b, c) {
return a+b+c
}
let _add = curry(add);
_add(a)(b,c)
_add(a)(b)(c)
函数add被curry后得到柯里化函数_add,_add能够处理add的所有剩余参数。因此柯里化也被称为部分求值。
简版实现:
// argsNow用于收集参数
// lenNow用于标记还剩几个参数没传进来,全部传进来了就可以执行了
function curry(fun,args=[],len=fun.length){
// 第一次创建_add函数的时候,没传args和len,也就是初始化函数时,参数数组为空
let argsNow = args
let lenNow = len
function changeFun(){
// 把参数收集起来
let rArgs = [...arguments]
argsNow = [...argsNow,...arguments]
// 如果没收集完就继续收集
if(rArgs.length<len){
lenNow -= rArgs.length
return curry(fun,argsNow,lenNow)
}
// 收集完了就执行
return fun(...argsNow)
}
return changeFun
}
花里胡哨!最后执行结果不还是执行了原函数?!柯里化就这?简单的问题反而给搞复杂了。
// 前面高阶函数的一个案例
function isType(type,obj){
return Object.prototype.toString.call( obj ) === '[object '+type+']';
};
// 请求
function ajax(method, url, data) {
return fetch(url, {
method: method, // or 'PUT'
body: data
})
}
通过柯里化我们可以得到:
let _isType = curry(isType)
let _ajax = curry(ajax)
let isString = _isType('string')
let isObject = _isType('object')
let getAjax = _ajax('get')
let postAjax = _ajax('post')
**
再看一个更具高阶函数思维的例子:
开发中,常用的数组map通常只有几类,所以我们可以做出如下封装。
function arrMap(func, array) {
return array.map(func);
}
let _arrMap = createCurry(arrMap);
let percenMap = _getNewArray(function(item) {
return item * 100 + '%'
})
let dubbleMap = _getNewArray(function(item) {
return item * 2
})
percenMap([0.01, 1]);
dubbleMap([10,20])
经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。
而且柯里化后的函数也明显的更加自由。
**
前面的高阶函数大家可以在实践中多尝试使用,但是柯里化不建议为了使用而使用。因为在js中,柯里化的实现是以性能为代价的,只有当情况变得复杂时,才是柯里化的主场。
尽管我们的例子都太简单了,简单到对他们进行柯里化显得多余,但是我还是希望能在这些简单的案例中学到柯里化的思维。在未来实践中,如果发现用普通的思维封装一些逻辑慢慢变得困难,最起码可以想到柯里化这样一个方案。
包括柯里化和前面所有的函数式编程的内容,当中没有什么新奇的东西或者要记的概念,更多的是一些编程思想上的东西,希望能对大家实际开发当中产生一点点的影响。