刚学几个函数,还不太熟,看见两个函数结果动情的女人结局都一样样,但形式不一样,麻烦帮忙解释一下

前端使用面向对象式编程 还是 函数式编程 针对什么问题用什么方式 分别有什么具体案例?
前端编程现在是函数式比较火 但是oo易于理解 起因ruanyf的weibo
和大家的讨论
按投票排序
前段时间看《JavaScript函数式编程》这本书时候写了读书笔记,在这里献一下丑。什么是函数式风格编程函数式风格编程包括了以下技术:确定抽象,并为其构建函数。这种抽象的粒度可以很细很细利用已有的函数构建更复杂的抽象通过将现有的函数传给其它的函数来构建更复杂的抽象从程序员的思路上来说,就是将程序拆分并抽象成多个函数,再组装回去。在JavaScript这门语言中,函数是一等公民,函数可以作为函数的参数,函数也可以返回一个函数,因此适合函数式编程风格。之所以使用了“函数式风格编程”,而不说“函数式编程”,是因为后者具有更为确切的标准,而JavaScript从语言本身来说就无法完全达到,因此只能说“具有函数式编程的风格”。为什么要函数式风格的编程函数式风格的编程,长于数据的处理,且所谓“处理”并不改变数据本身。JavaScript中,数据总是以对象的形式出现,也就是说,函数式的编程对于“接收对象A→处理→处理→处理→得出新对象B”这样的流程可以很优雅地实现,且不影响对象A本身。而且别忘了,函数本身也可以凭空生成数据。另一个特点是,函数式风格的编程讲究各个抽象互相独立,不发生耦合。这是这种编程风格的根本特性之一。对于JavaScript程序员(无论是前端还是后端),最常见且实用的编程模式当然是抽象出类(以构造函数的形式)并实例化,然后在实例上调用方法。函数式风格的编程不是为了取代这种编程模式,而是为了与之形成互补。一些特点不改变输入的数据不耦合,也就是说,函数尽量不影响甚至知晓外部的状态保持数据组织形式的简单,例如,尽量使用JavaScript原生的数据结构(对象、数组等)简单的实践:组合函数并处理数据引入术语“applicative编程”,定义是“把函数A作为参数提供给函数B”。我们的函数式风格编程从applicative编程开始起步。以Underscore.js定义的一些函数为例:_.map / _.mapObject_.fliter / _.reject_.reduce / _.reduceRight_.all / _.any.sortBy / _.groupBy / .countBy / _.indexBy_.find/ _.findIndex / _.findLastIndex / _.findKey这些函数都有以下特点:接收一个表示集合的数据(原生的对象或数组),并在不影响原数据的情况下,返回一个新的经过处理的数据接收一个函数作为第二个参数用以进行上一项提到的数据处理。即使传入的不是函数,也会被转化为函数(若传入对象,则调用.matcher将其转化为函数,其它则是调用.property转化为函数)。书中引用了图灵奖得主的话:“用100个函数操作一个数据结构,比用10个函数操作10个数据结构要好。”,从我的角度来理解,从易编写性、低耦合性上来说,多写几个函数比多写几个数据结构好得多,这正是多多使用applicative编程能带给我们的好处:让函数来接收函数,你就能应对各种情况(只需要传入不同的函数就好,而不是去设计、组织数据的结构)。当你连续地调用函数来处理数据时,你应当清楚每一层交接了什么东西;对于经常被连续调用的组合,可以再把它们抽象成函数。例如:function truthy(a) {
return a != undefined && a !== false
//剔除所有断言函数返回false的表中的内容
function restrict(table, predicate) {
return _.reduce(table, function(newTable, obj){
if (truthy(predicate(obj))) return newTable;
else return _.without(newTable, obj)
}, table);
高阶函数:用函数生成函数预备的理念:传入函数,而不是值接收函数作为参数,且返回一个函数作为结果的函数,叫做高阶函数。在上一节中我们的提到applicative编程风格是一种需要大量函数的编程方式,学习使用高阶函数来为其它函数提供函数,无疑更优雅、方便,降低了耦合性。为了贯彻“让函数接收函数”这一理念,我们引入以下函数:_.identity = function(value) {
return value
这是一个原样返回传入值的函数,看似没什么用,但这是函数式风格编程的一个体现:传入函数,而不是值(哪怕这个函数的工作很微小,甚至只是原样返回一个值)。这样一来,你就可以写出这样的代码:_.map([1,2,3,4], _.identity)。再引入一个很有函数式编程风格的代码片段:_.constant = function(value) {
return function() {
return value
_.times = function(n, iteratee) {
var accum = new Array(Math.max(0, n));
for (var i = 0; i & n; i++) accum[i] = iteratee(i);
return accum;
_.times(5, _.constant(42)); // [42, 42, 42, 42, 42]
其中,_.constant看似是一个没什么用的函数,但它通过制造函数,与_.times函数(以及其它applicative的函数)配合得非常好。总之,记得用函数来代替值。提供纯函数的API尝试着把“调用构造函数”、“调用对象的方法”这类的OOP风格的操作放在函数里:function getCarValue(a) {
return new Car(a).getValue;
这也是贯彻函数式风格编程的方法之一。开始编写高阶函数在这一节里会给出一些高阶函数作为例子。在此之前,说明一下高阶函数的一些用途:用高阶函数来生成函数并传给其它函数,践行“传入函数,而不是值”的理念。高阶函数可以根据自己接收的参数,来配置新函数的行为这是来自书中一个“对象校验器”的例子,体现了函数是如何被组装起来的:// 这是一个高阶函数
// 接收一个断言函数。返回一个新的,带有报错信息(message属性)的断言函数
// 在函数的组装中,负责产生原材料
function generateValidator(message, fun) {
var f = function() {
return fun.apply(fun, arguments)
f['message'] = message;
// 这是一个高阶函数
// 接收任意个带有报错信息的断言函数
// 返回一个新函数作为验证器,功能是逐一调用断言函数并拼接报错信息,即按照指定规则检验一个值
// 在函数的组装中,负责将原材料拼凑成成品
function generateValidatingScheme() {
var validators = _.toArray(arguments);
return function(obj) {
return _.reduce(validators, function(errs, check) {
if (check(obj)) return errs;
else return _.chain(errs).push(check.message).value();
// 指定规则,生成验证器
// 即获得了一个成品
var validatingScheme = generateValidatingScheme(
generateValidator('not a object', _.isObject),
generateValidator('not a boolean', _.isBoolean)
// 验证值42
// 即使用成品
validatingScheme(42); // ["not a object", "not a boolean"]
比较复杂的高阶函数在这节会学习一些利用高阶函数的高级技巧,生成一些便于参与组装的函数。柯里化将函数进行柯里化,可以使该函数的指定参数被固定下来(从数量到值都可以)。例如:// 这是最简单的柯里化函数,作用是将func函数固定为仅接收一个函数
function curry(func) {
return function(arg) {
return func(arg)
['11', '11', '11'].map(parseInt); // [11, NaN, 3]
['11', '11', '11'].map(curry(parseInt)); //[11, 11, 11]
// 这个柯里化函数可以把func函数的第二个参数固定
function curry2(func) {
return function(secondArg) {
return function(firstArg) {
return func(firstArg, secondArg);
// 将parseInt函数的第二个参数固定为2
['11', '11', '11'].map(curry2(parseInt)(2));// [3, 3, 3]
使用柯里化技巧,可以获得名字更具可读性的函数,便于你设计API,例如上例中的var parseBinary = curry2(parseInt)(2);。实践中,柯里化函数(如curry2)在固定func函数的参数时,通常是从左至右(右至左亦可)连续地固定(通过像curry3(func)(1)(2)这样连续调用)。不是说curry3函数不能做到像“固定第一个和第三个参数”这种,只是这么写,在日后调用curry3函数时会让人糊涂。对于这种情况,可以使用部分应用(partial)的技巧。部分应用(partial)这里偷个懒,把Underscore.js的实现贴上来(改成了ES6语法): _.partial = function (func, ...boundArgs) {
let placeholder = _.partial.placeholder; // 用户可以通过给_.partial传入与placeholder全等的值来跳过某些位置的预填充
return function bound() { // bound函数是调用_.partial后获得的函数
let position = 0, length = boundArgs.length;
let args = Array(length); // args是一个空数组,稍后把它传入apply作为完整参数列表
for (let i = 0; i & length; i++) {
args[i] = (boundArgs[i] === placeholder) ? arguments[position++] : boundArgs[i]; // 预填充数组里的某位若是占位符,则这个值从实际传参里取,否则从预填充数组里取
while (position & arguments.length) args.push(arguments[position++]); // 取出bound函数的实际传参里剩余的所有参数。至此args是预填充参数和实际传参的集合
// this的值即为bound函数被调用时的this值
if(!this instanceof bound) return func.apply(this, args);
// 若bound函数以非构造函数的形式被调用,则直接返回调用后的结果
// 以下情况考虑的是bound函数以构造函数形式被调用的情况
let self = Object.create(func.prototype), // 模拟new操作符的原理,构建一个以func.prototype为原型的对象
result = func.apply(self, args);
if(_.isObject(result)) return result; // 模拟new操作符的原理,若调用func后得到一对象则抛弃之前构造的对象
return self;
_.partial.placeholder = _; // 占位符可以由用户更改。每次更改之后,对之前_.partial制造的函数不生效
你也许已经发现,JavaScript原生的Funciton.prototype.bind方法正是“部分应用”的一种不完整实现,缺陷在于只能连续地将参数固定下来(也就是柯里化),而我们的函数更高级一些。在获得了“部分应用”这个得力工具后,让我们看看如何把上一大节的例子与这节的成果加以整合:// 我们可以进一步利用上面的成果:
// 这一函数期望接收若干个generateValidator的返回值
// func在调用前,确保传入的参数是有效的
function generateFunctionWithConditions() {
var validators = _.toArray(arguments);
return function(func, arg) {
var errors = [];
validators.forEach(function(isValid){
errors = isValid(arg) ? [] : errors.concat(isValid.message);
if(!errors.length === 0) throw new Error(errors.join());
else return fun(arg);
// 一个不安全的求平方函数。例如,传入字符串也会得出计算结果
var square = function(a) {
return a*a;
//获得一个安全的求平方函数
var squareSafe = _.partial(generateFunctionWithCondition(
generateValidator('can\'t be zero', function(num){ num !== 0}),
generateValidator('can\'t be string', _.isString)
), square);
squareSafe('1'); // 报错
squareSafe('0'); // 报错
从这个例子可以看出,函数式编程风格的难点在于知道自己手里有什么工具、应当制造什么工具,以及怎样用已有工具组合出新工具。拼接一个流水线函数一个理想化的函数式程序是向函数流水线的一端输入一块数据,从另一端输出全新的数据块。说到拼接函数流水线,值得一提的是Underscore.js里的_.compose函数。流水线很强大,除了按顺序让函数陆续地处理数据,也可以有别的用途,例如,结合我们之前的generateFunctionWithCondition函数,我们可以做到检查的后置:var squareSafe = _.compose(_.partial(generateFunctionWithCondition(
generateValidator('can\'t be zero', function(num){ num !== 0}),
generateValidator('can\'t be string', _.isString)
), _.identity) ,square);
我们可以把这样的流水线视为对某一核心函数的“装饰”(上例中的核心函数是square),且“装饰”可以随意添加(顺序、个数、功能)。搭配curry与parital,可以构建出非常流畅的流水线。另一个好消息是,_.compose函数的实现非常简单:_.compose = function () { // 这个函数接收多个函数作为参数,并产生一个新函数
var args = arguments;
var start = args.length - 1;
return function () {
// 新函数连续调用多个参数以处理数据
var i = start;
var result = args[start].apply(this, arguments); // 首先执行最后一个函数
while (i--) result = args[i].call(this, result); // 将得到的结果作为参数传入倒数第二个函数,以此类推
return result;
// 返回最终结果
更复杂的函数流水线,请参考下文“基于流的编程”一节。函数的其它技巧递归自递归编写任何自递归函数仅需遵循以下三条:确定一个停止条件决定怎样算一个步骤把问题分解成一个步骤和一个较小的问题以一个获得数组长度的自递归函数为例:functioni getLength(arr) {
if(_.isEmpty(arr)) return 0; // “确定一个停止条件”
else return 1 + getLength(_.rest(arr)) // “较小的问题”→_.rest(arr)。“一个步骤”→ 1 + ...
注意,有时我们可能需要让递归的函数维护一个计数器以确定是否到达停止条件,此时使用嵌套的函数可以避免:function checker(){
var predicates = _.toArray(arguments);
return function() {
var args = _.toArray(arguments);
var everything = function(preds, truth) { // 用嵌套的函数来消耗predicates函数们
if (_.isEmpty(preds)) return truth;
// “确定一个停止条件”
else return _.every(args, _first(preds)) && everything(_.rest(preds), truth) // “一个步骤”→ _.every(args, _first(preds)),“较小的问题”→_.rest(preds)
return everything(preds, true);
相互递归两个或多个函数互相调用称为相互递归函数。这样的递归同样遵循上面的三条原则。函数A把自己传入.map,就是一种A和.map相互递归的体现:function flat(array) {
if(!_.isArray(array)) return [array];
// “确定一个停止条件”
else return Array.prototype.concat.apply([], _.map(array, flat)); //
“一个步骤”→
Array.prototype.concat.apply([], ...)
显然,“较小的问题”的分解这一职责需要相互递归的函数中的一个或多个来承担(上例中由_.map负责)。问题在来回调用的过程中变得越来越小。蹦床技巧来避免栈溢出递归易于理解、易于编写,但缺点在于若计算量很大,可能造成尚未返回的函数太多,导致栈溢出。此时应当使用蹦床技巧,基本原理是避免在函数中立即调用函数,而是用函数生成函数:function even(n) {
if(n === 0) return true;
else return _.partial(odd, Math.abs(n - 1)); // 在用蹦床技巧改进前,是return odd(Math.abs(n - 1))
function odd(n) {
if(n === 0 ) return false;
else return _.partial(even, Math.abs(n - 1)); // 用蹦床技巧改进前,是return even(Math.abs(n - 1))
function trampoline(func) {
var result = func.apply(null, _.rest(arguments));
while(_.isFunction(result)) { // 手动展平
result = result();
return result;
function isEven(n) {
if(n === 0) return true;
else return trampoline(odd, Math.abs(n - 1)) ;
使之一目了然:基于流的编程在这一节,我们会探讨一些链式风格的函数组装方式,以及更复杂的流水线函数。通过这类基于流的编程,可以使数据的处理流程一目了然,并易于更改流程。惰性的链式调用Underscore.js提供了_.chain函数来进行链式调用。这种链式API,在每一步都会计算、求值,即使你并未调用value方法。这有时是一种局限;另外,有时需要先设定好一个调用链,但真正的调用时刻在其它时间点(且需要重复多次),这也是惰性链式调用的用武之地。接下来展示一种惰性链式调用API的实现。// 接收一个数据对象作为原料
// 也可以接收一个LazyChain对象,以将其的调用链延长
function LazyChain(obj) {
var isLC = obj instanceof LazyChain;
this._calls = isLC ? this._calls.concat(obj._calls) : [];
this._target = isLC ? obj._target : obj;
// 记住将要调用什么方法,以及之后调用时的参数
LazyChain.prototype.invoke = function(methodName, ...args){
this._calls.push(function(target) {
var meth = target[methodName];
return meth.apply(target, args)
return this;
// 真正地调用所有登记好的方法
// 有可能对原始数据造成影响
LazyChain.prototype.force = function() {
return _.reduce(this._calls, function(target, thunk){
return thunk(target); // target是上一个函数的返回值
}, this._target);
流水线与控制流链式调用总是要求传入的原始数据对象本身具有能用的方法,且每一步最好返回一个具有与原始数据对象的类型相同的对象,否则有可能无法调用根据方法名登记好的方法(除非你记得每一步后返回的对象具有哪些能用的方法)。而流水线机制,由于使用来自数据之外的函数,因此灵活得多。在上文,我们简单地使用Underscore.js的_.compose来实现函数流水线。这节我们尝试一种其它实现,并将其扩展为适应性更强的版本。function pipeline(seed, ...funcs) {
return _.reduce(funcs, function(l, r){ return r(l)}, seed);
pipeline函数很原始。虽然允许接收任意个外部的函数,看似耦合性很低,但这些函数的返回值必须按照一定的规矩,例如,pipeline(42, a, b,c )时,若b函数返回的是undefined,则期待接收数字的c函数就失效 ,导致整个流程失效,下面是一个很好的改进:function generatePipeline(...funcs) {
return function(seed) {
return _.reduce(funcs, function(l, r){var result = r(l); return result == null ? l : result}, seed);
当然这还不足以应对“函数a的返回值的格式与函数c期望的参数的格式不同”这一困境,解决的方法是利用“适配器模式”改进函数a,或者在generatePipeline函数内,为所有函数包装一个统一格式的返回值。原则:纯度与不变性纯函数具有以下特性:其返回值仅仅根据参数进行计算不能依赖于能被外部操作改变的数据自身亦不改变外部状态编写越多的纯函数,代码就越易于测试、易于阅读理解、易于解耦、易于组合、易于别人使用。组合纯函数,得到的也是纯函数。基于上述特点,结合JS的特性,可以发现:任何接收引用类型的JS函数都有可能不纯。利用了闭包的函数也是不纯的。
面向对象编程和函数式编程的差别当然远比阮老师说的差别要大, 他不是混 Haskell 或者 Scheme 社区的, 只是从前端提一个侧面. 的答案写得比较详细了, 基本赞成, 我写不了那么详细, 补充一些侧面. 考虑截图里有 Scheme 代码, 找个写 Scheme 的扯扯可能更多料.Scheme 里添加闭包, 意图是说, OOP 里能做的, FP 里也能做, 并不是说 FP 用闭包就是整个 OOP 的东西了. OOP 包罗万象, 或说是概念太杂了, 拿个小车做例子, 小车定义类型时拆开各个部分算 OOP, 小车属性改变算 OOP, Java 里一堆的概念算 OOP, js 里用一下 prototype 模拟个东西出来算 OOP. 一会又是 Smalltalk 作者说 OOP 就是消息传递, 一会儿函数式编程语言 Elrang 被称为是唯一贴合 OOP 的语言. 概念太杂了.OOP 当中很多套路 FP 也在用, 比如说定义类型时定义对应的结构和部分, 然后定义方法来修改数据, 然后一整体为单位封装起来抽象起来用在运算当中.Object Oriented Programming. It is a type of programming in whichprogrammers define not only the data type of a data structure, but also the types of operations (functions) that can be applied to the data structure. In this way, the data structure becomes an object that includes both data and functions.Object-oriented programming (OOP) is a
based on the concept of "", which may contain , in the form of , often known as
and code, in the form of procedures, often known as . A distinguishing feature of objects is that an object's procedures can access and often modify the data fields of the object with which they are associated (objects have a notion of "" or "self"). In OO programming, computer programs are designed by making them out of objects that interact with one another. There is significant diversity in object-oriented programming, but most popular languages are , meaning that objects are
of , which typically also determines their .以我熟悉的语言举例子, 在 Go 当中定义 interface, 先定义 struct 说这个数据有什么属性, 然后定义函数声明这个 struct 有什么方法, 这个看上去和 OOP 的说法一致吧, 大家说 Go 的面向对象很弱. 或者 Haskell 当中 定义一下 Record 类型的数据, 或者用 Data 定义一下新的类型, 然后声明一下它继承一些已有的类型类, 定义一些方法, 也符合上边的 OOP 的概念吧, 大家说 Haskell 是纯函数式编程语言.举这个例子主要想说 OOP 概念太混杂了, 各种语言里有各种概念. 我没深入学 Java, Java 里定义 class 各种概念, 数都数不过来. 而 js 当中单单 OOP 又同时又两种, 一种从 Self 演变过来的 prototype 的写法, 一种基于 class, 可能还有其他奇怪的闭包写法或者什么. 我觉得这仅仅是通过模仿在实现 OOP.那么模仿的话, 它的语法什么样子, 它用什么概念, 途径就很多了. js 当中因为在数据类型层面模仿了 OOP, 所以 method 调用是一种写法. 而截图当中模拟函数式的写法, 前面有人说了, 还有柯理化的写法, 大致一样的意思, 也可以写得很不一样, 我建议看下 Scheme 里模仿 OOP 的写法
.回答题主的问题, 什么分别? 首先在 js 当中是语法的不同, 图片上是一种. 但我觉得 js 真的, 作为脚本语言混杂了太多的概念, 事情反而说不清楚. 我认为在 OOP 和 FP 当中最大的分歧是变量和属性究竟能不能修改, 其他语言和语法的细节还好说. OOP 里不讨论这个问题, 默认是可变的(插入评论,java 有手册推荐 immutable,而 js 在 react 之前极少提 immutable 现在当然风风火火。之前说是鼓励可变,有问题). 但是在 Haskell Clojure 这系列的语言当中, 数据不可修改的. 很多 Lisp 方便是可以的, 比如 Scheme, 我记得 Scheme 是 1973 年的吧, Haskell 是 1990 的, Clojure 是 2007 年的, 数据凭印象写的, 但你明白我的意思吧, Scheme 太老了, 后面的语言干脆就不妥协了. 数据不可修改带来巨大的难题, 首先 for while 不能写了, 1++ 是修改数据, 被迫用尾递归, 或者是封装过的内置函数 map reduce 等等, 当然还有各种问题, 逼自己不修改数据试试就知道了... 当然 js 因为是脚本, 而且内置了前面的 map reduce 等等, 最直观的区别还是语法上.FP 在之前影响到前端, 估计还是语法的差别, 手写过程式代码不容易出错而且抽象和复用差, FP 能起到不小的作用, 这是早就明确的. 真的 FP 热到现在的程度还是 React 导致的, 不可变数据, 嵌套组件等等. 真的感兴趣还是去学 Haskell 吧, , React 当中还是大量的 OOP 混杂, 因为 DOM 太难对付了.OOP 写法更好理解, 原因是 js 自己号称是 OOP 语言, 在语法上就支持了 OOP 的写法, 当然看起来更漂亮了. 但我一直强调, js 是个大杂烩, 所以要弄明白函数式编程, 还是基于纯函数的语言讨论, 比如 Haskell Clojure.
bark居然有两个参数?人家都是先把它 curry 化,把它变成 one-argument pure functionconst bark = _.curry((how, who) =& { ... })
const barkLoudly = bark(loudly)
咦?dog 实例呢?哈哈,fp 哪有数据。=============== 分割线 ===============FP 其实挺适合前端开发,JavaScript 有 First-Class Function,使得函数可以用变量存储起来;函数也可以作为参数传入函数,也可以作为函数的返回结果(所谓的 Higher-Order Function)。这样就使得 JS 可以使用很多 FP 的玩法。FP 的 Pure Function 的概念我觉得在前端非常适用。一个函数的计算返回结果只依赖于它本身的参数,并且它在计算的过程中没有对外部的状态进行修改(也就是传说中的没有副作用),那么就可以把这个函数称之为 Pure Function。举个例子:let a = 0;
const add = (b) =& a + b; // impure
const add2 = (a, b) =& a + b; // pure
const changeInnerHTML = (id) =& document.getElementById(id).innerHTML = 'good'; // impure
add 函数计算过程中依赖外部变量 a, 所以你调用它的时候根本不知道它会返回什么样的结果;而 add2 则不会,因为它的结算结果只依赖于它的两个参数。然而 changeInnerHTML 函数也是 impure 的,因为它修改了 DOM 的状态,也就是产生了“副作用”。前端工程的复杂性其实主要是由状态管理造成的。用户点击某个按钮,页面显示 Loading,然后发送请求去后台请求数据,最后在页面上进行显示。这个过程你需要维护一个加载的状态,后台的数据,页面的状态。假设你现在写了好几个函数,这几个函数都可以修改这些状态,那么你就需要小心翼翼地,因为你根本不知道哪些函数会改掉某个变量。当你查看某个函数的逻辑的时候,你可能需要翻越很长的代码去看看另外那个函数对这个变量做了什么。所以我觉得有句话说得很有道理:Shared mutable state is the root of all evil但是状态修改是无法避免的(你肯定要操作 DOM 对吧),但是我们可以尽量减少共享状态,尽量多地写 Pure Function。Pure Function 可以组成另外的 Pure Function,然后小心翼翼地用一个 Impure Function 把数据灌进去,然后把计算结果小心翼翼地灌倒另外一个 Impure Function 里面。重点就是尽量地把没有副作用的 Pure Function 当作是管道那样连接起来,数据流过这些管道。这里有两个比较重要的地方,一个是 composition,一个是 currying。currying 帮助你实现 one-argument function,一个参数的函数。composition 帮助你实现 one-argument 的函数拼接。这样做好处多多:1. 代码高度模块化,复用性强2. Pure Function 的可测试性无敌3. 维护代码成本降低,大脑负载降低。因为对于 Pure Function 你根本不用在乎外部的变量。4. 声明式编程可以让你在更高的思维层次进行编码感觉 currying 和 composition 可以重点讲讲。currying估计很多人刚学 JS 的时候都学过这玩意儿:两个数相加:const add = (a, b) =& a + b
但是现在每次都要传 a, b 进去,如果我现在实现一个 +1 的功能,每次都要 add (a, b),我们可以把这个函数 currying 化:const add = (a) =& (b) =& a + b
现在 add(1) 会返回一个函数,而不是直接计算结果。const add1 = add(1)
const num = add1(3) // =& 4
这样就很好了,我们把一个函数的参数从两个变成了一个。使用这种技术,我们可以把一个多参数的函数搞成一个参数的函数。虽然你学过 currying ,但是可能根本没用过,但是 currying 技术非常好用实用(本人天天用,安利一波)。而实际上,有很多第三方的库可以帮我们把一个函数 currying 化。例如大名鼎鼎的 lodash:import _ from 'lodash/fp'
const add = _.curry((a, b) =& a + b)
const add 1 = add(1)
const num = add1(3) // =& 4
所以,对于阮老师的栗子我觉得更好的写法是:import _ from 'lodash/fp'
const bark = _.curry((how, who) =& { ... })
const barkLoudly = bark(loudly)
现在 barkLoudly 是一个 one-argument function了。compositioncomposition 指的就是函数之间的组合,可以让你把各个 one-argument pure function 像管道那样连接起来。例如你要让一条狗叫,你先要有一个条狗,所以你先要有钱买狗。从大局上来看,我们输入的是 money 输出的是狗叫。这就是我们的大局的 pure function,而这个 pure function 是由几个的 pure function 组合而成。const barkLoudly = bark(loudly)
const makeMoney = (num) =& {...}
const buyOneDog = _.curry((type, money) =& {...})
const buyLabrador = buyOneDog('Labrador')
const dogApp = (money) =& barkLoudly(buyLabrador(makeMoney(money)))
dogApp(3000)
在这个例子里面,我们把 buyOneDog currying 化了,两个参数变成了一个参数的 buyLabrador,所以这代码的含义就是,用了三千块买了一只拉布拉多,它叫得很大声。但是 f(g(h())) 这种方式看着实在蛋疼,其实它就是把一个函数的结果作为参数传入另一个函数,再把结果传给另外一个函数,如此往复。而也有现成的 lib 帮我们让它变得更好看一点:const dogApp = _.compose(barkLoudly, buyLabrador, makeMoney)
// === const dogApp = (money) =& barkLoudly(buyLabrador(makeMoney(money)))
所以完整的写法应该是:import _ from 'lodash/fp'
const bark = _.curry((how, who) =& { ... })
const barkLoudly = bark(loudly)
const makeMoney = (num) =& {...}
const buyOneDog = _.curry((type, money) =& {...})
const buyLabrador = buyOneDog('Labrador')
const dogApp = _.compose(barkLoudly, buyLabrador, makeMoney)
dogApp(3000)
看到了吗,除了最后一行调用,其他都是函数。认真地观察,可以看到一个整数3000是怎么流过一个管道变成另外一个东西,然后再流过另外一个管道,最后变成了狗叫。还有很多玩法,甚至还可以玩 monad。待续再写写...
UPDATE:上点代码阮老师举的这个例子,excited。如果 bark 的实现是这样……const bark = (animal, volume) =& dog.bark(volume)
所以真·函数式 bark 要怎么玩?从函数式的角度来看 dog 和 bark, bark 明显是一个会产生副作用的操作(比如 console.log 一下)。而 dog ,应该是一种类型,包含几个字段……具体有哪些字段先不关心了,我们姑且认为它有一个 barking: "Woof woof!"于是,我们可以用上 monad 。先定义一个简单的 IO monad 类型const IO = fn =& function (arg){
this._val = fn(arg) || null // 懒得定义空值了,就用null好了
this.bind = transform =& transform(this._val)
return this
然后我们可以定义一个IO monad bark :const bark = IO(myDog =& console.log(myDog.barking))
然后就可以愉快的 bark 了:bark({ barking: "Woof woof!" })
……“写这么多代码才让狗叫起来,是不是有点硬点的意思呢?”monad: 怪我咯?我本来也不是拿来让狗叫的啊_(:з」∠)_(monad 在 JavaScript 中的实现和用途可以看看)--------------所有的范式信仰都是耍流氓(中央已经决定了)在 GUI 编程中面向对象是一种很自然也很广泛也很实用的编程套路。而在 JavaScript 中,函数式编程更是无处不在。只要程序猿用了 callback 就是用了函数式编程的思想和方法。现在你有一个对象,你调用对象方法的时候传了一个 callback 。你这是在 Object-oriented 还是在 Functional Programming ?函数式编程和面向对象编程,在 JavaScript 里没有任何冲突的意思。题主不要听风就……到时候报道出了偏差……又有人说什么“JavaScript 社区以幼稚和愚昧著称”……无中生有的事……相当于……你也有责任吧。
OOP 不需要语言提供 class 等等,实际上 OOP 只要能实现 duck typing 就行了。这和 FP 并不矛盾。参见 SICP 第二章还是第三章我忘了
关键是前端的主力语言Javascript,既不是纯的OO语言,也不是纯的Functional语言,而是为了实用融合了两者概念的一种语言。所以用Javascript解决问题,程序员总是不知不觉地穿梭在两种思想之中。Javascript的function foo() {},一被写出来,就既是Class(Object),也是Function。所以foo同时是OO和Functional的多面体,被传来传去的,既可以以Object的身份,也可以以高阶函数的身份,究竟是什么取决于如何被使用。Javascript的使用,随着各种构建越来越复杂,已经不那么直观了,比如说Observable(RxJS),听起来像Object,但核心其实是包好了的Function。
泻药为啥又得搀和这些破烂事儿阮大师贴的东西我认为就是个玩笑(虽然有人认为他是认真的)拿纯函数式来说几个重要特点1、不可变数据2、柯里化3、尾递归优化(不可变数据导致纯函数式不应有循环自变量,所以循环用递归代替,得有这个来防止爆栈)4、等等在他这个简陋的例子里都没体现而他博客里有文章是介绍函数式的你说怎么让我相信他不是玩笑除非大师真是blog大师(当然这个也是可能的,我劲量忘好地方想)如果真是认真的(当然这也有很大可能性,毕竟……你们懂的)那看其他答案就好了基本上已经回答的很详细了回到问题上前端用的JS其实这两点基本都具备(比如OO度和函数度)但都不完备所以别教条主义郑人买履要不得得根据你自身实际情况和客观环境来决定主要使用哪个或者混合使用(比如你要非要玩纯函数式没尾递归优化那项目能活么还)(比如非要整纯面向对象模拟各种for in copy 属性项目也够呛啊)
泻药,瞎答。我对函数式编程不是特别了解,纯个人理解,如果非要往狗叫上套。先看下函数式编程的几个常见要素:1,不依赖于外部的数据,而且也不改变外部数据的值,返回新的值给你。2,函数可以当成变量来使用,可以输入也可以输出。3,递归或者尾递归。4,状态传递或者说pipeline形式。打比方,dog有3个方法 bark,bite,run对应的面向对象写法:dog.bark("loudly"); dog.bite("harder"); dog.run("faster");log(dog);这没什么好解释的。那么函数式应该怎么写呢。log(barkLoudly(biteHarder(runFaster(dog)))); 或者这样:log(bark(bite(run(dog,"faster"),"harder"),"loudly"));又或者这样:log(dog,[run_faster,bite_harder,bark_loudly]);再或者这样?dog.run("faster").bite("harder").bark("loudly").log(); //哈哈…这个应该不算…...我编不下去了。我觉得这样举例子可能更清晰一些……勿喷,我还是喜欢面向对象的写法…能上代码,咱就少说话…评论区有各种update。。
首先吐槽一下,图片的狗叫例子里,那个所谓的函数式编程,既不是函数式编程,也未必不是面向对象编程(C里面写面向对象也就这样子,只是受语言的限制。)那么什么是函数式风格?函数式风格其实是从控制流程的层面来说的,它区别于「过程式」风格。函数式的根本特征是注重函数演算,无论是高阶函数也好,函数组合也好,都是为了这点。所以常常以函数为参数、返回值。这样的目的是为了减少代码的耦合、增加函数重用率,以及延迟计算。很多人把面向对象跟函数式对立起来,根本原因是混淆了「过程式」和「面向过程」两个概念。过程式,英文是Procedural programming,中文也叫「命令式」风格,它派生自结构化编程,结构化编程是什么,就是「采用子程序、代码区块、for循环以及while循环等结构,来取代传统的 goto」,这个跟面向对象有没有冲突?没有冲突。重申一下,过程式风格是在「控制流程」的层面的说法,而「面向对象」、「面向过程」是设计层面的说法。面向过程,英文是Procedure Oriented,是一种以事件处理为中心的设计理念,一个程序就是一堆功能函数的集合。它跟面向对象不同,数据结构起的作用只为记录,所以面向过程也有「面向记录」的别名。所以说,「过程式」和「面向过程」不是一回事。传统的面向对象编程,从控制流程来说,它是过程式的。但是无论过程式也好,函数式也好,一旦要做面向对象,都要花更多功夫。而函数式风格又是猿类刚刚才重新捡回来的,难免觉得跟传统面向对象不和,需要磨合。就目前来说,面向对象编程里加了函数式风格之后,会有一些变化,譬如,更多构建小对象,而少用大对象(相对而言),少使用类继承,多使用接口和函数等等。至于前端如何使用函数式编程,首先我们要明白,JavaScript其实就是一门多范式语言,有「批着C外衣的Lisp」之称。那么我们首先要看看JavaScript有些什么函数式特征,有些什么函数式编程的技巧。这个推荐先看「JavaScript语言精粹」。多接触,多写,随心所欲就好了。
面向对象的感觉是,一个东西,做了一个动作。面向函数的感觉是,一堆数据,做了一次映射。
angular2的pipe正好用到了这两种思想。另外绝大多数jq插件都是面向对象编程,所有的中间件,包括redux中间件,express中间件等都是函数式编程。关于函数式编程,也可以看这个回答,
已有帐号?
无法登录?
社交帐号登录

我要回帖

更多关于 爱自己和谁结婚都一样 的文章

 

随机推荐