函数式编程

函数式编程是一种编程范式,和面向对象编程呈并列关系。

  • 面向对象编程:对现实世界中事物的抽象,抽象出对象以及对象和对象之间的关系;
  • 函数式编程:把现实世界事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)。

推荐书籍

  • 你不知道Javascript
  • Javascript忍者秘籍
  • Javascript20years

函数是一等公民

  • 函数可以存储在变量中;
  • 函数作为参数;
  • 函数作为返回值。

原理:在js中函数的数据类型为对象。

高阶函数

可以把函数作为参数/返回值。

意义:屏蔽细节,便于抽象。

常用的高阶函数:

  • forEach
  • map
  • filter
  • every:检测数组中的所有元素是否符合条件,有一个不符合返回false
  • some:检测数组中是否函数符合条件,有一个符合返回true
  • find/findIndex:返回数组中满足条件的第一个元素的值/索引,没有则返回undefined、-1
  • reduce:接收一个函数作为累加器,数组中的值从左到右,上一个输出作为下一次迭代的输入,最后返回一个值。可以作为一个高阶函数用于函数组合
  • sort

this指向

改变函数this指向对方法:bind(不调用)、call、apply

  • 模拟bind实现
// 模拟bind实现
Function.prototype.myBind = function (context, ...args) {
    return (...rest) => this.call(context, ...args, ...rest)
}

执行上下文

  • 全局执行上下文

  • 函数级执行上下文
    函数的执行阶段可以分为:
    1.函数建立阶段:当调用函数时,还没有执行函数内部的代码

    • variableObject(VO):收集函数中的arguments、参数、内部成员;
    • scopeChains:词法环境,作用域链,记录当前函数所在父级作用域中的活动对象;[[Scopes]]作用域链,函数在创建时就会生成该属性,js引擎才可以访问,这个属性中存储的是所有父级中的变量对象。
    • this:当前函数内部的this指向,this的指向是动态确定的,当函数在调用时才能确定。

    2.函数执行阶段

    • activationObject(AO):用AO指向VO
  • eval执行上下文

闭包

能够读取其他函数内部变量的函数。

本质:函数在执行时会被放入执行栈,当函数执行完毕会从执行栈上移除,但堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

作用:
1.可以读取函数内部的变量;
2.让这些变量的值始终保持在内存中。

使用注意点:
1.由于闭包会使函数中的变量都被保存在内存中,因此滥用闭包会造成性能问题。解决方法:在退出函数之前,删除所有无用的变量。
2.不要随意修改父函数中变量的值。

// once
function once(fn) {
    let done = false
    return function () {
        if (!done) {
            done = true
            // arguments:类数组对象,存储传入函数的所有参数。具有数组的部分属性,比如.length、索引
            // apply(obj, args):
            // 劫持另一个对象的方法,继承另一个对象的属性,obj代替function中的this对象,args作为参数传递给函数args-->arguments
            return fn.apply(this, arguments)
        }
    }
}

纯函数(⚠️重点掌握)

相同的输入永远会得到相同的输出,类似于数学中的函数关系,没有任何可观察的副作用(副作用不可能完全禁止)。

纯函数的好处
  • 可缓存
// 模拟memorize
function memorize(fn) {
    let cache = {}

    return function () {
        let arg_str = JSON.stringify(arguments)
        cache[arg_str] = cache[arg_str] || fn.apply(fn, arguments)
        return cache[arg_str]
    }
}
  • 可测试
  • 并行处理

柯里化(⚠️重点掌握)

当一个函数有多个参数时先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个参数接收剩余的参数,返回结果。

对函数进行降维,为函数组合作准备。

lodash中的柯里化函数
// 模拟_.curry()的实现
function curry(fn) {
    // ...args-剩余参数:将不定量的参数表示为一个数组
    // 剩余参数和arguments的区别:
    // 1.剩余参数只包含那些没有对应形参的实参,而arguments包含传给函数的所有实参;
    // 2.arguments对象不是一个真正的数组,而剩余参数是一个真正的Array实例;
    // 3.arguments还有一些剩余的属性。
    return function curriedFn(...args) {
        // 判断实参和形参的个数
        // function.length -> 函数形参的个数
        if (args.length < fn.length) {
            return curriedFn(...args.concat(Array.from(arguments)))
        }
        return fn(...args)  // args展开
    }
}

函数组合(⚠️重点掌握)

如果一个函数要经过多个函数处理才能得到最终值,可以把中间过程的函数组合为一个函数。顺序默认从右向左执行。

// 模拟lodash中的flowRight
function compose(...fns) {
    return function (value) {
        return fns.reverse().reduce(function (acc, fn) {
            return fn(acc)
        }, value)  // value为初始值
    }
}

// 使用钩子函数
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)
如何调试函数组合

可以编写一个接收“当前位置标记”和value并返回value的柯里化函数,插入到函数组合中,用于追踪哪一步出错。

// 'NEVER SAY DIE' --> 'never-say-die'
// 调试
const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})

const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, arr) => _.join(arr, sep))
const map = _.curry((fn, arr) => _.map(arr, fn))

// 错误
const f = _.flowRight(join('-'), trace('map之后'), _.toLower, trace('map之前'), split(' '))
// 正确
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('map之前'), split(' '))

console.log('NEVER SAY DIE');
lodash/fp

提供了对函数式编程友好的方法,提供了不可变自动柯里化函数优先数据滞后的方法。

Functor(函子)

函子就是一个装有一个变量的容器,通过map方法维护这个变量。

函子在开发中的实际应用场景:作用是空值副作用(IO)、异常处理(Either)、异步任务(Task)。

// Functor 函子
// 函数式的编程不直接操作值,而是由函子完成
class Container{
    // of静态方法,可以省略new关键字创建对象
    static of(value) {
        return new Container(value)
    }

    constructor(value) {
        this._value = value
    }

    // map方法,传入处理value的函数,返回一个包含新值的函子
    map(fn) {
        return Container.of(fn(this._value))
    }
}

MayBe函子

// MayBe 函子
// 处理空置异常
class MayBe {
    static of(value) {
        return new MayBe(value)
    }

    constructor(value) {
        this._value = value
    }

    map(fn) {
        return MayBe.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }

    isNothing() {
        return this._value === null || this._value === undefined
    }
}

Either函子

// Either函子
// 传入空值调用的函子
class Left {
    static of(value) {
        return new Left(value)
    }

    constructor(value) {
        this._value = value
    }

    map(fn) {
        return this
    }
}

// 正常情况下调用的函子
class Right {
    static of(value) {
        return new Right(value)
    }

    constructor(value) {
        this._value = value
    }

    map(fn) {
        return Right.of(fn(this._value))
    }
}

// Either用来处理异常
function parseJSON(json) {
    try {
        return Right.of(JSON.parse(json))
    } catch (e) {
        return Left.of({ error: e.message })
    }
}

IO函子

IO函子中的_value是一个函数,可以把不纯的动作存储到_value中,延迟这个不纯的操作。

const fp = require('lodash/fp')

// IO函子
class IO {
    static of(value) {
        // 给当前传入的value包裹一层函数,让不纯的操作滞后发生,保证当前函数相同输入得到相同输出。
        // 将不纯的操作交给调用者处理。
        return new IO(function () {
            return value
        })
    }

    constructor(fn) {
        this._value = fn
    }

    map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}

Task异步执行

// folktale中的task函子,处理异步执行
function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) resolver.reject(err)
            resolver.resolve(data)
        })
    })
}

// 调用run执行
readFile('package.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run()
    .listen({
        onRejected: err => {
            console.log(err)
        },
        onResolved: value => {
            console.log(value)
        }
    })

Pointed函子

是实现了of静态方法的函子,更深层的意义是将值放到上下文Context(把值放到容器中,使用map处理值)。

Monad(单子)

是可以变扁的Pointed函子,具有join和of两个方法,能够解决IO函子中出现的多层嵌套问题。

const fp = require('lodash/fp')

// IO Monad
class IO {
    static of(value) {
        return new IO(function () {
            return value
        })
    }

    constructor(fn) {
        this._value = fn
    }

    map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    }

    // 返回调用后的值,减少一层嵌套
    join() {
        return this._value()
    }

    // 拍平map
    flatMap(fn) {
        return this.map(fn).join()
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 在前端快速发展的今天,如果不能时刻保持学习就会很快被淘汰。分享一下最近学习的函数式编程的相关知识,希望对大家有所帮...
    gongyexj阅读 745评论 0 0
  • Study Notes[https://wuner.gitee.io/wuner-notes/fed-e-task...
    Wuner阅读 518评论 0 0
  • 拉勾大前端的笔记,仅作为学习记录 课程介绍 为什么学习函数式编程,以及什么是函数编程 函数式编程的特性(纯函数,柯...
    yapingXu阅读 278评论 0 3
  • 函数式编程范式 为什么学习函数式编程 函数式编程是随着react的流行受到了越来越多的关注 vue 3也开始拥抱函...
    _咻咻咻咻咻阅读 179评论 0 0
  • 第一章: 函数式编程主要基于数学函数和它的思想。 1.1 函数与js方法: 函数是一段可以通过其名称被调用的代码,...
    yuhuan121阅读 11,882评论 5 8