本文章记录本人在学习 函数式 中理解到的一些东西,加深记忆和并且整理记录下来,方便之后的复习。
在近期看到了《JavaScript函数式编程》这本书预售的时候就定了下来。主要目的是个人目前还是不理解什么是函数式编程。在自己学习的过程中一直听到身边的人说面向过程编程和面向对象编程,而函数式就非常少。为了自己不要落后于其他同学的脚步,故想以写笔记的方式去分享和记录自己阅读中所汲取的知识。
书中用了一句简单的话来回答了什么是函数式编程:
函数式编程通过使用函数来将值转换为抽象单元,接着用于构建软件系统。
我觉得一定有同学看了这一句还是不怎么动什么是函数式编程,且为什么要使用函数式编程。后面的很多例子都使用到了 Underscore
。
抽象方法是指隐藏了细节的函数。举一个书中的例子,一个检测输出年龄值的函数(主要是关于错误和警告的报告):
function parseAge(age) { if (!_.isString(age)) throw new Error("Expecting a string"); var a; console.log("Attempting to parse an age"); a = parseInt(age, 10); if (_.isNaN(a)) { console.log(["Could not parse age: "].join()); a = 0; } return a; }
上面的函数判断我们是不是输入一个年龄,且必须是字符串形式。接着就是来运行这个函数:
parseAge("42"); //=> 42 parseAge(42); //=> Error:Expecting a string parseAge("hhhh"); //=> 0
上面的 parseAge
函数工作正常没有什么问题。如果我们要修改输出错误=信息和警告的呈现方式、那么就需要修改相应的代码行,以及其他地方的输出模式。书中给的方法是通过将它们抽象成不同的函数来实现:
function fail(thing) { throw new Error(thing); } function warn(thing) { console.log(["WARNING:", thing].join('')); } function note(thing) { console.log(["NOTE:", thing].join('')); }
接着就是使用上面的函数,去重构 parseAge
这个函数。
funciton parseAge(age) { if (!_.isString(age)) fail("Expecting a string"); var a; note("Attempting to parse an age"); a = parseInt(age, 10); if (_.isNaN(a)) { warn(["Could not parse age:", age].join("")); a = 0; } return a; }
把报告错误的代码都放到不同的函数里去,且重构后的 parseAge
和之前的也没有多大的变化。但是不同的就是现在报告错误、信息和警告的想法已经被抽象化。错误、信息和警告的报告结果也是完全被修改了。
这么做是,由于行为包含在单一的函数中,所以函数可以被能够提供类似行为的新函数取代,或直接被完全不同的行为取代。
这个标题很容易理解,举个例子。像我们经常使用 iife
来避免全局的污染,这就是一个封装和隐蔽的很好例子。通过使用 iife
来隐蔽自己的写的一些变量和方法,目的就是不去污染全局的环境。这也是使用闭包的方式来隐蔽数据。
因为闭包也是一种函数。且和现在在学习函数式编程有莫大的关系。但是也不要忘记了之前学习的面向对象式封装,毕竟这两者不能说谁更加的好。但是都掌握了也不是一件什么坏事。一句老话:看需求。
隐藏数据和行为(通常不方便于快速修改)只是一种讲函数作为抽象单元的方式。另一种方式就是提供一种简单地存储方式和传递基本行为的离线散单元。
书中一个小栗子,通过使用 js
语法来索引数组中的一个值:
var arr = ['a', 'b', 'c']; arr[1] //=> b
虽然上面索引数组中的中的一个值很简单,但并没有办法可以在不把它放到函数里的前提下,获取这个行为并根据需要来使用他/她。写一个简单函数 nth
,用来索引数组中的一个值:
function nth(a, index) { return a[index]; }
接着运行:
nth(arr, 1); //=> b
运行成功,但是如果传入一个空对象时,就会报错了。因此,如果想围绕 nth
来实现函数抽象,我们或许会设计下面的声明: nth
返回一个存储在允许索引访问的数据类型中的有效袁术。这段声明的关键在于索引数据的类型的概念。或许需要一个函数来判断类型:
function isIndexed(data) { return _.isArray(data) || _.isString(data); }
接着继续完善 nth
函数。 isIndexed
函数是一个提供了判断某个数据是否为字符串或者数组的抽象。
function nth(a, index) { if (!_.isNumber(index)) fail("Expected a number as the index"); if (!isIndexed(a)) fail("Not supported on non-indexed type"); if ((index < 0) || (index > a.length - 1)) fail("Index value is out of bounds"); return a[index]; }
从 index
抽对象构建 nth
函数抽象的方式一样,也可以以同样的方式来构建一个 second
抽象:
function second(a) { return nth(a, 1); }
函数 second
允许在一个不同但相关的情况下,正确的使用 nth
函数:
second(arr); //=> b
通过上面的栗子,就知道。我们可以把每一步都抽象成一个函数,把每一个参数都抽象出来。虽然这样写感觉定义了许多函数。不过这样更加容易理解每一项的功能和流程。
JavaScript 的对象原型模型是一个丰富且基础的数据方案。
因为 js
没有类的原因,就有了许多模拟类的方法,且在 ES6
上也出现了 class
关键字。尽管类有许多长处,但很多的时候 js
应用程序的数据需求币类中的简单的要多。
基于类的对象系统的一个有理的论据是实现用户界面的历史使用。
js
中的对象和数组已经能够满足我们对数据的操作了,且 Underscore
也是重点也是如何处理数组和对象。
实施和使用的简易性是使用 js
的核心数据结构进行数据建模的目的。这并不是说面向对象或者基于类的方法就完全没有用。处理集合为中心的函数式方式更加适合处理与人有关的数据,而面向对象的方法最适合模拟人。
在开始函数式编程前,需要先定义两个常用且有用的函数:
function existy(x) { return x != null } function truthy(x) { return (x !== false) && existy(x); }
existy
函数旨在定义事物之前的存在。 js
中就有两个值可以表示不存在: null
和 undefined
。 truthy
函数用来判断一个对象是否应该认为是 true
的同义词。
我们可以在很多地方使用到这两个函数,其实函数式理念来自于它们的使用。有些同学可能已经熟悉了许多 js
实现中的 map forEach
等方法。且 Underscroe
也提供了许多类似的方法,这也许就是选择 Underscroe
来辅助学习函数式编程的原因。
简单说下就是:
大概了解了函数式编程之后。你可能会想这函数式编程不是很慢吗?比如前面获取数组索引,有必要定义一个函数来专门获取吗?直接用 arr[index]
绝对比那些函数来的快。
var arr = [1, 2, 3, 4, 5]; // 最快 for (var i = 0; i < arr.length; i++) { console.log(arr[i]); } // 较慢 _.each(arr, function (val, index) { console.log(index); });
但是我们在写代码的时候可能不会考虑的那么深,也许使用函数的确比原生要慢一些。但是大多数情况下也不会去在乎那么点时间,且现在有强大的 v8
引擎,大部分情况下的他都能很高效的编译和执行我们的 js
代码。所以我们没有必要在还没有写出正确的代码前考虑运算速度。
如果是我来选择的话,可能会更加关注与代码的风格。那种写法写的舒服看的舒服就使用哪一种,当然也是要保证基本的运算速度下,以不至于慢的离谱。看的舒服的代码比跑的快的代码可能更加有成就感。
看完了第一章也是可以小结一下 js
的函数式编程。下面引用书上的总结:
单是构建抽象还是不够的,如果能够把强大的数据抽象结合来实现函数式编程效果会更加好。