Skip to content

Instantly share code, notes, and snippets.

@riskers
Last active November 21, 2022 09:32
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save riskers/637e23baeaa92c497efd52616ca83bdc to your computer and use it in GitHub Desktop.
Save riskers/637e23baeaa92c497efd52616ca83bdc to your computer and use it in GitHub Desktop.
javascript 函数式编程

我眼中的函数式

函数式是一种编程范式,面向对象也是一种编程范式。

函数式分为两类,Lisp 和 Haskell,Lisp又有很多方言,Clojure、Scheme 都是其中一种。

JS 函数式

JavaScript 这个语言是基于原型的,可以用构造函数的方法去写 OOP,也可以去写函数式。

lodash/fpRamda.js 是优秀的JS函数式库,实现了函数式所需要的各种功能,比如 curry、compose 等等。

因为 Haskell 太学术,在工程中很难使用,所以出现了 ElmClojureScriptElm 是一门能编译成 JS 的函数式语言。ClojureScript 则是 Clojure 在 JS 的实现。

至于 rxjs ,是一个响应式、函数式库,它用 iterator、pub-sub 两种模式来实现响应式。cyclejs 则是基于 rxjs 的一个完整 web 前端框架。

响应式编程不是 js 的专利,reactive 有很多语言的实现 就像函数式编程不是 js 的专利,但是有很多库实现的函数式,比如 lodash/fp ,Ramda.js


学习过程

| - Haskell -> Elm
| - Lisp -> Clojure
| - JS 函数式 -> Ramdajs (lodash/fp)
| - rxjs -> cyclejs
  • 20170825: Haskell 学习了60%,JS函数式90%,差不多可以进入下一个阶段 rxjs 了
  • 2019年底: 持续学习
  • 2022年底

高阶函数

就是操作函数的函数,接受一个或多个函数作为参数,并返回一个新函数。

纯函数

数学中,函数 f 的概念就是对于输入x,产生一个输出y=f(x),就是对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

var arr = [1,2,3,4,5];

// Array.prototype.slice 是纯函数,没有副作用,对于固定的输入,输出也总是固定
arr.slice(0,3); //[1,2,3]
arr.slice(0,3); //[1,2,3]

// Array.prototype.splice 不纯,有副作用,对于固定输入,输出不固定
arr.splice(0,3);  //[1,2,3]
arr.splice(0,3);  //[4,5]

函数式编程中,我们想要的就是 slice 这样的纯函数,而不是 splice 这样的每次调用后会把数据弄乱的函数。

// 不纯的 checkage 函数的行为不仅取决于输入参数age,还取决于外部变量 min
// 即函数的行为需要由外部系统环境决定
// 一旦需求复杂,则造成了系统的复杂性
var min = 18;
var checkage = age => age > min;

// 纯的,很函数式
var checkage = age => age > 18;

可以看到,纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性差,所以需要用到函数的柯里化

柯里化 curry

柯里化 定义:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

比如 var add = (x,y) => x+y; 可以这样:

var add = function(x){
	return function(y){
		return x + y;
	}
}

//es6
var add = x => (y => x + y);

var add2 = add(2);
add2(2);	//4

var add100 = add(100);
add100(2);	//102

这样简单的函数使用柯里化没什么意义,比如之前的 checkage :

var checkage = function(min){
    return function(age){
        return age > min;
    }
}
var checkage18 = checkage(18);
checkage18(20);	//true
// 判断数据类型
function isType(type){
    return function(obj){
        return Object.prototype.toString.call(obj) === "[object " + type + "]"
    }
}

var isString = isType('String')
var isNumber = isType('Number')
var isBoolean = isType('Boolean')

为什么要柯里化呢?

函数的柯里化能够让你重新组合你的应用,把你的复杂功能拆分成一个个小部分,每一个小部分都是简单的,便于理解的,容易测试的。


如何柯里化?

function print(name){
    return function(age){
        return name + 'age is:' + age
    }
}

print 函数虽然进行了柯里化,但是我们肯定不想每次在需要柯里化的时候,都像上面那样进行函数的嵌套,那是噩梦!所以我们需要一个帮助其它函数进行柯里化的函数:

function curry(fn){
	var args = Array.prototype.slice.call(arguments,1);	// 除 fn 外的参数
	return function(){
		var newArgs = Array.prototype.slice.call(arguments);	// 新函数的全部参数
		var totalArgs = args.concat(newArgs);	// 合并的参数
		return fn.apply(this,totalArgs);
	}
}

下面使用 curry 来柯里化 showMessage

function showMessage(name,age,height){
    console.log(name)
    console.log(age)
    console.log(height)
}

var sm = curry(showMessage,'yy')
sm(25,190)
  • args -> ['yy']
  • newArgs -> [25,190]
  • totalArgs -> ['yy',25,190]

柯里化使用场景

  1. 可以使用一些小技巧

  2. 提前绑定好函数里面的某些参数,达到参数复用的效果,提高了适用性.

// 兼容事件绑定
var addEvent = function (el, type, fn, capture) {
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);
    }
    else {
        el.attachEvent('on' + type, fn);
    }
};

//但是上面每次都会运行 if ,产生不必要开销,我们可以这样:

var addEvent = (function () {
    if (window.addEventListener) {
        return function (el, type, fn, capture) {
            el.addEventListener(type, fn, capture);
        }
    }
    else {
        return function (el, type, fn) {
            var IEtype = 'on' + type;
            el.attachEvent(IEtype, fn);
        }
    }
})();
  1. 固定易变因素

Function.prototype.bind

  1. 延迟计算
function add() {
    var args = Array.prototype.slice.call(arguments);
    var _that = this;
    return function() {
        var newArgs = Array.prototype.slice.call(arguments);
        var total = args.concat(newArgs);
        if(!arguments.length) {
            var result = 1;
            for(var i = 0; i < total.length; i++) {
                result *= total[i];
            }
            return result;
        }
        else {
            return add.apply(_that, total);
        }
    }
}
add(1)(2)(3)(); // 6
add(1, 2, 3)(); // 6

函数组合

学会使用纯函数和柯里化后,容易写出『包菜式』代码:

h(g(f(x)))

虽然这也是函数式,但是不优雅,我们需要用到函数组合:

var compose = function(f,g){
	return function(x){
		f(g(x))
	}
}

var add1 = x => x + 1
var mul5 = x => x * 5

compose(mul5,add1)(2)	//15

Point Free

Point Free

中文意思是,不要命名转瞬即逝的中间变量

// str 作为中间变量,除了让代码变长了一点外,没有别的意义
var f = function(str){
	str.toUpperCase().split(' ')
}

// 改造一下
var toUpperCase = function(word){
	return word.toUpperCase()
}
var split = function(x){
	return function(str){
		return str.split(x)
	}
}
var f = compose(split(' '),toUpperCase);
f('abcd efgh')

这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。

为什么需要函数式?

  • 可读性高,逻辑上更容易理解
  • 更高的抽象层级,代码高度可复用

函数式概述

函数式最大的主旨就是编程中所有过程可控,尤其是js这种没有原则的语言中,过程可控尤为重要。

函数式:函数是一等公民,能作为变量的值,可以是另一个函数的参数,可以返回另一个函数。

回想一下 jq 的链式操作,是不是每次一个链接加上去就立马生效,当你写完一堆链式之后,发现不对,如果不能一眼看出是哪步错了,只能一次次去删最后的操作来找原因。而惰性链是在你写完链式时候并不会执行,而是在最后跟上一个执行用的函数,才会去执行前面的所有函数。

柯里化与偏应用函数

高阶函数

JavaScript 中,函数是一等公民。除了可执行单元外,函数还可以被作为参数传入别的函数,也可以作为其他函数的返回值。

比如 _.map 就可以接受一个函数作为参数,因此它就是一个高阶函数的实例。

// 高阶函数例子
function refresh(url,callback){
	$.get(url).done(callback);
}

function update(data){ $('#container').html(data) }

// 此处的 refresh 的调用看起来和 add(1,2) 并无区别,函数 update 作为参数传入,正如普通函数调用一样
refresh("./alarms.json",update);

高阶函数也是柯里化和偏函数的基础。

柯里化

柯里化是指函数返回一个函数,这个返回的函数携带了一些预定义信息,当调用这个返回的函数时,就可以省略掉那些预定义的参数了

function add(x){
	return function(y){
		return x + y;
	}
}
var inc = add(1)
var dev = add(-1)

_.map(_.range(1,5),inc);	//[2,3,4,5]

也可以这样使用 add(10)(2) ,这个函数携带了 10

偏应用函数

简称偏函数,和柯里化相似,不过调用方式非常不同,用途也不同。

主要应用到了 bind ,可接受的参数分为两部分:

  • 作为执行时函数上下文中的this的对象
  • 函数的参数
// updater 接受两个参数,一个是容器id,一个是数据来源url
function updater(container,url){
	console.log(url,container)
}

如果 container 是固定的,那么我们可以这样构造:

var updaterLiked = updater(null,'#liked')
updaterLiked('./data.json')

_.partial :

var n = _.partial(updater,'#liked')
n('./data.json')	

var n = _.partial(updater,_,'data.json')
n('#liked')

组合

函数式真正强大之处:单个函数可以被组合起来完成更为复杂的操作

_.compose 会从右到左依次应用

var greet    = function(name){ return "hi: " + name; };
var exclaim  = function(statement){ return statement.toUpperCase() + "!"; };
var welcome = _.compose(greet, exclaim);
welcome('moe'); //=> 'hi: MOE!'

underscore 链式操作

得到一段文字中单词出现次数:

var text = "Hi my name is Hi"

function text2word(text){
    return _.map(text.match(/\w+/g),function(word){
        return word.toLowerCase()
    })
}

function count(words){
    return _.reduce(words,function(pre,cur){
        pre[cur] = (pre[cur] || 0) + 1
        return pre;

    },{})
}

count(text2word(text))

链式操作为:

_.chain(text.match(/\w+/g))
    .map(function(word){
        return word.toLowerCase()
    })
    .reduce(function(pre,cur){
        pre[cur] = (pre[cur] || 0) + 1
        return pre;
    },{})
    .value()

value() 为取出链式操作的值

可以看到,使用 underscore 提供的众多 API,可以写出非常简洁的代码来,这也是函数式编程逐步占领编程世界的原因。

@riskers
Copy link
Author

riskers commented Nov 21, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment