函数式编程入门

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

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'

函数组合的例子1

// 使用组合前

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 函数存在

函数组合的例子2

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的例子

// 非 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库

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值