函数式编程入门

一、 面向过程编程、面向对象编程、函数式编程概要

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库

原文地址:https://www.cnblogs.com/xiaoeshuang/p/14452485.html