以下大部分金句摘抄自 Franklin Risby教授的《JS函数式编程指南》
我非常喜欢这种风格的写作方式,我不喜欢抱着一本泰戈尔的《飞鸟集》去看,也欣赏不了莎士比亚的歌剧,我喜欢的是在教授编程技巧的时候不时引入两句我不懂的话,再过了一段时间能让我猛然回头深夜重新打开灯翻看。在讲述编程技巧的时候不时引入一两句经典台词,就算我并没有听懂他教授我的编程技巧,但还是觉得我们在思想上有共鸣,这种感觉非常美妙,让我感觉他就坐在我的身旁。
这本书我看了好几年了,每次都是到第五章就坚持不下去了,然后忘的一干二净。所以这次我把这本书中的金句和让“我悟了”的代码摘抄下来。这本书中的金句是真的多,可以多背一下,作为下一家工作的求职中与面试官侃侃而谈的资本。
以下正文
《JS函数式编程指南》
第1章:我们在做什么
JavaScript 也更容易入门,因为它是一门混合范式的语言,你随时可以在感觉吃力的时候回退到原有的编程习惯上去。
这门语言完全有能力书写高级的函数式代码只需借助一到两个微型类库,JavaScript 就能模拟 Scala 或 Haskell 这类语言的全部特性。 虽然面向对象编程(Object-oriented programing)主导着业界,但很明显这种范式在 JavaScript 里非常笨拙,用起来就像在高速公路上露营或者穿着橡胶套鞋跳踢踏舞一样。 我们不得不到处使用bind以免this不知不觉地变了,我们还发明了各种变通方法来应对忘记调用new关键字后的怪异行为,私有成员只能通过闭包(closure)才能实现,等等。对大多数人来说,函数式编程看起来更加自然。
现在已经有一些通用的编程原则了,各种缩写词带领我们在编程的黑暗隧道里前行:DRY(不要重复自己,don’t repeat yourself),高内聚低耦合(loose coupling high cohesion),YAGNI (你不会用到它的,ya ain’t gonna need it),最小意外原则(Principle of least surprise),单一责任(single responsibility)等等。
第2章:一等公民函数
当我们说函数是“一等公民”的时候,我们实际上说的是它们和其他对象都一样…所以就是普通公民(坐经济舱的人?)。函数真没什么特殊的,你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量…等等。
多此一举的回掉函数
世界上到处都充斥着这样的垃圾 ajax 代码。以下是上述两种写法等价的原因
function ajaxCall(call) {
call("req")
}
// V1
const fn1 = function (callback) {
return ajaxCall(
/**
* 这三行优化掉,callback本就是一个接收一个参数的函数了,
* 为何要再定义一个额外的包裹函数,而它仅仅是用这个相同的参数调用callback?完全没有道理。
* 这就像在大夏天里穿上你最厚的大衣,只是为了跟热空气过不去,然后吃上个冰棍。
* start
*/
(json) => {
return callback(json);
}
/** end */
);
}
fn1((res) => { console.log(res, 'fn1') })
// V2 同理优化掉包裹的function
const fn2 = function (callback) {
return ajaxCall(callback);
}
fn2((res) => { console.log(res, "fn2") })
// V3 最终版本
const fn3 = ajaxCall // <-- 看,没有括号哦
fn3((res) => { console.log(res, "fn2") })
各位,以上才是写函数的正确方式。一会儿再告诉你为何我对此如此执着。
再看一个例子
var BlogController = (function () {
var index = function (posts) {
return Views.index(posts);
};
var show = function (post) {
return Views.show(post);
};
var create = function (attrs) {
return Db.create(attrs);
};
var update = function (post, attrs) {
return Db.update(post, attrs);
};
var destroy = function (post) {
return Db.destroy(post);
};
return { index: index, show: show, create: create, update: update, destroy: destroy };
})();
这个可笑的控制器(controller)99% 的代码都是垃圾。我们可以把它重写成这样:
var BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy
};
为何钟爱一等公民
如果一个函数被不必要地包裹起来了,而且发生了改动,那么包裹它的那个函数也要做相应的变更。
httpGet('/post/2', function(json){
return renderPost(json);
});
如果httpGet要改成可以抛出一个可能出现的err异常,那我们还要回过头去把“胶水”函数也改了。
// 把整个应用里的所有 httpGet 调用都改成这样,可以传递 err 参数。
httpGet('/post/2', function(json, err){
return renderPost(json, err);
});
写成一等公民函数的形式,要做的改动将会少得多:
httpGet('/post/2', renderPost); // renderPost 将会在 httpGet 中调用,想要多少参数都行
同一概念不同命名
项目中常见的一种造成混淆的原因是,针对同一个概念使用不同的命名。还有通用代码的问题。比如,下面这两个函数做的事情一模一样,但后一个就显得更加通用,可重用性也更高
// 只针对当前的博客
var validArticles = function(articles) {
return articles.filter(function(article){
return article !== null && article !== undefined;
});
};
// 对未来的项目友好太多
var compact = function(xs) {
return xs.filter(function(x) {
return x !== null && x !== undefined;
});
};
在命名的时候,我们特别容易把自己限定在特定的数据上(本例中是articles)。这种现象很常见,也是重复造轮子的一大原因。
小心this
有一点我必须得指出,你一定要非常小心this值,别让它反咬你一口,这一点与面向对象代码类似。如果一个底层函数使用了this,而且是以一等公民的方式被调用的,那你就等着 JS 这个蹩脚的抽象概念发怒吧。
var fs = require('fs');
// 太可怕了
fs.readFile('freaky_friday.txt', Db.save);
// 好一点点
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
把 Db 绑定(bind)到它自己身上以后,你就可以随心所欲地调用它的原型链式垃圾代码了。this就像一块脏尿布,我尽可能地避免使用它,因为在函数式编程中根本用不到它。然而,在使用其他的类库时,你却不得不向这个疯狂的世界低头。
也有人反驳说this能提高执行速度。如果你是这种对速度吹毛求疵的人,那你还是合上这本书吧。要是没法退货退款,也许你可以去换一本更入门的书来读。
第 3 章:纯函数的好处
副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
副作用
slice和splice
在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像splice这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。
来看看另一个例子。
// 不纯的
var minimum = 21;
var checkAge = function (age) {
return age >= minimum;
};
// 纯的
var checkAge = function (age) {
var minimum = 21;
return age >= minimum;
};
在不纯的版本中,checkAge的结果将取决于minimum这个可变变量的值。换句话说,它取决于系统状态(system state);这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。
这个例子可能还不是那么明显,但这种依赖状态是影响系统复杂度的罪魁祸首(http://www.curtclifton.net/storage/papers/MoseleyMarks06a.pdf)。输入值之外的因素能够左右checkAge的返回值,不仅让它变得不纯,而且导致每次我们思考整个软件的时候都痛苦不堪。
副作用可能包含,但不限于:
- 更改文件系统
- 往数据库插入记录
- 发送一个 http 请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM 查询
- 访问系统状态
这个列表还可以继续写下去。概括来讲,只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。
这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。
追求“纯”的理由
可缓存性(Cacheable)
var squareNumber = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25
下面的代码是一个简单的实现,尽管它不太健壮。
lodash也提供了这个函数 https://www.lodashjs.com/docs/lodash.memoize
var memoize = function (f) {
var cache = {};
return function () {
var arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
可移植性
见名知意,与系统状态耦合的函数移植肯定是比纯函数移植问题大的多了,如果面试官说让解释一下,那可以转身拍屁股走人了。
第 4 章: 柯里化(curry)
我父亲以前跟我说过,有些事物在你得到之前是无足轻重的,得到之后就不可或缺了。微波炉是这样,智能手机是这样,互联网也是这样——老人们在没有互联网的时候过得也很充实。对我来说,函数的柯里化(curry)也是这样。
curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
var add = function (x) {
return function (y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
这里我们定义了一个add函数,它接受一个参数并返回一个新的函数。调用add之后,返回的函数就通过闭包的方式记住了add的第一个参数。一次性地调用它实在是有点繁琐,好在我们可以使用一个特殊的curry帮助函数(helper function)使这类函数的定义和调用更加容易。
我已经开始有点懵了,现在还搞不懂这背后的设计哲学
记录一个lodash curry使用的方式吧
var curry = require('lodash').curry;
var match = curry(function (what, str) {
return str.match(what);
});
console.log(match(/\s+/g, "he l l o w o r l d"), "匹配出现的空格")
// [ ' ' ]
console.log(match(/\s+/g)("he l l o w o r l d"),"使用curry方式匹配出现的空格")
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
我没觉得这样分两次调用的方式在业务代码中有用,水平有限,现在的我看到这种写法只能认作为是炫技,或许在你的眼睛里,和我认为的一样吧。不过该背的还是得背,面试官问“curry化有什么好处:”通过简单地传递几个参数,就能动态创建实用的新函数。
第 5 章: 代码组合(compose)
这就是组合(compose)
var compose = function (f, g) {
return function (x) {
return f(g(x));
};
};
f和g都是函数,x是在它们之间通过“管道”传输的值。
组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数。组合的用法如下:
使用:
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
这一章已经够了
下边的八年级数学,范畴学什么的我放弃了,如果我和你讨论某个编程技巧的时候扯到这些,你也会认为我是不懂装懂吧。
第6章:示例代码
不要误会,这一章的title就叫示例代码
命令式和声明式编程
与命令式不同,声明式意味着我们要写表达式,而不是一步一步的指示。
以 SQL 为例,它就没有“先做这个,再做那个”的命令,有的只是一个指明我们想要从数据库取什么数据的表达式。至于如何取数据则是由它自己决定的。以后数据库升级也好,SQL 引擎优化也好,根本不需要更改查询语句。这是因为,有多种方式解析一个表达式并得到相同的结果。
对包括我在内的一些人来说,一开始是不太容易理解“声明式”这个概念的;所以让我们写几个例子找找感觉。
// 命令式
var makes = [];
for (i = 0; i < cars.length; i++) {
makes.push(cars[i].make);
}
// 声明式
var makes = cars.map(function (car) { return car.make; });
命令式的循环要求你必须先实例化一个数组,而且执行完这个实例化语句之后,解释器才继续执行后面的代码。然后再直接迭代cars列表,手动增加计数器,把各种零零散散的东西都展示出来…实在是直白得有些露骨。
使用map的版本是一个表达式,它对执行顺序没有要求(惰性求值\及早求值)。而且,map函数如何进行迭代,返回的数组如何收集,都有很大的自由度。它指明的是做什么,不是怎么做。因此,它是正儿八经的声明式代码。
除了更加清晰和简洁之外,map函数还可以进一步优化,这么一来我们宝贵的应用代码就无须改动了。
至于那些说“虽然如此,但使用命令式循环速度要快很多”的人,我建议你们先去学学 JIT 优化代码的相关知识。这里有一个非常棒的视频,可能会对你有帮助。
就这样吧,这次我又坚持不下去了,后边还有monda,下次吧
原书阅读地址
http://shouce.jb51.net/js-function/index.html
总结(非这本书的最后一章)
黑体加粗的文字都是要背过,要考的!
以下是我关于JS函数式编程意淫的问题
面试官问:你怎么理解js?
答:JavaScript 入门非常容易,因为它是一门混合范式的语言,你随时可以在感觉吃力的时候回退到原有的编程习惯上去,balabala…..
问:什么是一等公民?
答:你可以像对待任何其他数据类型一样对待它们——把它们存在数组里,当作参数传递,赋值给变量
问:JS在做面向对象过程中有哪些与主流oop语言没有的坑?
答:不得不到处使用bind以免this不知不觉地变了,我们还发明了各种变通方法来应对忘记调用new关键字后的怪异行为,私有成员只能通过闭包(closure)才能实现
问:纯函数有什么好处?
- 什么是副作用?
- 为什么追求纯?
答:副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
- 可缓存、移植性、测试性
在不纯的函数中,函数执行的结果将取决于外部可变变量的值。换句话说,它取决于系统状态(system state);这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。
这种依赖状态是影响系统复杂度的罪魁祸首,输入值之外的因素能够左右checkAge的返回值,不仅让它变得不纯,而且导致每次我们思考整个软件的时候都痛苦不堪。
问:什么是柯里化?
- 柯里化有什么好处?
答:curry 的概念很简单:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
- 通过简单地传递几个参数,就能动态创建实用的新函数。
问:什么是组合?
答:组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数。
问:什么事命令式和声明式编程?
答:指明的是做什么,不是怎么做