文章内容输出来源:拉勾教育大前端高薪训练营
已经学习了两周半拉钩教育大前端课程了,课程质量真的是好得没话说,我看过很多前端的课程,但从没有哪家课程能将前端的知识体系划分的如此全面细致,还能保证每一个知识点还都能讲得如此透彻,在讲知识点的基础上还能开篇幅去讲思想,更是难得。比如下面的函数式编程,这种编程范式我之前从来都没使用过,更不知道柯里化、函数组合为何物。直到在拉钩大前端课程中,每一个知识点的学习,都让我有种重获新生的感觉,仿佛以前学习的东西都白学了,只知道简单的用法,不了解核心原理,更不会用高级特性。现在每学习完一个模块,就期待着解锁下一个模块,迫不及待地想去知道下一个模块可以让自己get到哪些技能。
课程的主讲老师,汪磊老师,我看过他的webpack专栏,那时我就非常佩服他能够把webpack这样一个大而繁琐的工具,讲得如此细微易懂,让我懂了webpack的插件机制和loader机制。在大前端课程中,汪磊老师更是让我敬佩,我感觉他的知识面非常广,说他什么都懂也不为过,他还总是把我们在学习中会遇到的问题演示出来,或者是提出来让我们注意。最感谢汪磊老师的地方,就是在JS异步章节,直播课中的补充中,老师演示了各种function会影响this问题,道出了this取决于调用而不是定义,让我醍醐灌顶,也彻底搞懂了JS的this的取值,那天晚上令我激动地睡不着觉。
除此之外,两位助教老师还整天在群内答疑,只要遇到不懂的地方,就可以立马去群里问助教老师,老师会看到问题就会立马回复,如果是代码执行问题,还会把你的代码下载下来亲自运行排查,真的是太贴心了。班主任老师会在群里每天督促同学们交作业,遇到软件问题、作业提交问题、听课问题都可以找班主任老师。
一个人学习或许会太孤独,但是在拉钩教育大前端课程里,每天和几百人一起学习,群里还有专业的助教老师答疑,其他同学很多都是前端大佬,在你遇到问题的时候,无论是什么问题,只要是前端问题,总会有人给你解答或者提供思路。
在拉钩教育大前端课程中,一起学习,使彼此共同成长。
使用高阶函数的意义:抽象可以帮我们屏蔽细节,只需要关注于我们的目标。高阶函数是用来抽象通用的问题。
function forEach (array, fn) {
for (let i = 0; i < array.length; i++) {
fn(array[i])
}
}
function filter (array, fn) {
const res = []
for (let i = 0; i < array.length; i++) {
if(fn(array[i])) {
res.push(array[i])
}
}
return res
}
const arr = [1, 2, 4, 5, 2]
forEach(arr, console.log)
console.log(filter(arr, function (item) {
return item % 2 === 0
}))
function makeFn () {
let msg = 'hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn() // hello function
makeFn()() // hello function
应用:once函数 只执行一次的函数,比如说支付情况,无论用户点多少次,这个函数都只执行一次
function once(fn) {
let done = false
return function () {
if(!done) {
done = true
fn.apply(this, arguments)
}
}
}
let pay = once(function (money) {
console.log(`支付了${money}元`)
})
pay(1) // 支付了1元
pay(2)
pay(3)
常用的高阶函数:
forEach/map/filter/every/some/find/findIndex/reduce/sort
// 模拟常用的高阶函数:map every some
const arr = [1, 2, 3, 4]
// map
const map = (arr, fn) => {
let result = []
for(let item of arr) {
result.push(fn(item))
}
return result
}
console.log(map(arr, val => val * val)) // [ 1, 4, 9, 16 ]
// every
const every = (arr, fn) => {
for(let item of arr) {
if(!fn(item))return false
}
return true
}
console.log(every(arr, v => v > 0)) // true
// some
const some = (arr, fn) => {
for(let item of arr) {
if(fn(item))return true
}
return false
}
console.log(some(arr, v => v % 2 == 0)) // true
函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。可以在一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员。
本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈移除,但是堆上的作用域成员因为外部引用不能释放,因此内部函数依然可以访问外部函数的成员
function makeFn () {
let msg = 'hello function'
return function () {
console.log(msg)
}
}
const fn = makeFn()
fn() // hello function
闭包的应用:
function makePower(power) {
return function (num) {
return Math.pow(num, power)
}
}
// 求平方
let power2 = makePower(2)
let power3 = makePower(3)
console.log(power2(4))
console.log(power2(5))
console.log(power3(4))
相同的输入永远会得到相同的输出
没有任何可观察的副作用
类似数学中的函数
lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
数组的slice和splice分别是纯函数和不纯的函数
// 纯函数slice和不纯函数splice
let arr = [1, 2, 3, 4, 5]
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))
console.log(arr.slice(0, 3))
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))
console.log(arr.splice(0, 3))
function getSum(n1, n2) {
return n1 + n2
}
console.log(1, 2)
console.log(1, 2)
console.log(1, 2)
函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理
// 演示 lodash
// first last toUpper reverse each includes find findIndx
const _ = require('lodash')
const arr = ['jal', 'cathy', 'yibo', 'lucy']
console.log(_.first(arr))
console.log(_.last(arr))
console.log(_.toUpper(_.first(arr)))
console.log(_.reverse(arr))
const r = _.each(arr, (item, index) => {
console.log(item, index)
})
console.log(r)
缓存纯函数结果案例:
// 记忆函数
const _ = require('lodash')
function getArea (r) {
console.log(r)
return Math.PI * r * r * r
}
// let getAreaWithMemory = _.memoize(getArea)
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// console.log(getAreaWithMemory(4))
// 模拟memoize的实现
function memoize(fn) {
const cache = {}
return function () {
let key = JSON.stringify(arguments)
cache[key] = cache[key] || fn.apply(fn, arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 不纯的,函数的返回值依赖外部的变量
let mini = 18
function checkAge (age) {
return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge2 (age) {
let mini = 18
return age >= mini
}
副作用让一个函数变得不纯(如上例的checkAge中的mini是全局的),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用的来源:
配置文件
数据库
获取用户的输入
所有的外部交互都有可能代理副作用,副作用也是的方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序员带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。
// 柯里化演示
// function checkAge (age) {
// let mini = 18
// return age >= mini
// }
// 普通的纯函数
function checkAge (mini, age) {
return age >= mini
}
console.log(checkAge(18, 20))
console.log(checkAge(18, 24))
console.log(checkAge(22, 24))
// 闭包,高阶函数,函数的柯里化
function saveMini (mini) {
return function (age) {
return age >= mini
}
}
// ES6 写法, 同上
// const saveMini = mini => age => age >= mini
const checkAge18 = saveMini(18)
const checkAge22 = saveMini(22)
console.log(checkAge18(20))
console.log(checkAge18(24))
console.log(checkAge22(24))
_.curry(func)
// _.curry(func)
const _ = require('lodash')
function getSum (a, b, c) {
return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3)) // 6
console.log(curried(1)(2, 3))// 6
console.log(curried(1, 2)(3))// 6
案例:
// 柯里化案例
// ''.match(/\s+/g)
// ''.match(/\d+/g)
const _ = require('lodash')
const match = _.curry(function (reg, str) {
return str.match(reg)
})
const haveSpace = match(/\s+/g)
console.log(haveSpace('hello world')) // [ ' ' ]
console.log(haveSpace('hello')) // null
const haveNumber = match(/\d+/g)
console.log(haveNumber('123abc456def789')) // [ '123', '456', '789' ]
console.log(haveNumber('jal')) // null
const filter = _.curry(function (func, arr) {
return arr.filter(func)
})
console.log(filter(haveSpace, ['hello world', 'Ji Ailing', 'cathy', 'yibo', 'Wang Yibo']))
// [ 'hello world', 'Ji Ailing', 'Wang Yibo' ]
const findSpace = filter(haveSpace)
console.log(findSpace(['hello world', 'Ji Ailing', 'cathy', 'yibo', 'Wang Yibo']))
// [ 'hello world', 'Ji Ailing', 'Wang Yibo' ]
function getSum (a, b, c) {
return a + b + c
}
const myCurried = curry(getSum)
console.log(myCurried(1, 2, 3)) // 6
console.log(myCurried(1)(2, 3))// 6
console.log(myCurried(1, 2)(3))// 6
function curry(fn) {
return function curriedFn (...args) {
if(args.length < fn.length) {
return function () {
return curriedFn(...args.concat(Array.from(arguments)))
// return fn(...args, ...arguments) // 这样写也是一样的
}
}else {
return fn(...args)
}
}
}
如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。
function compose(f, g) {
return function (value) {
return f(g(value))
}
}
function reverse (arr) {
return arr.reverse()
}
function first (arr) {
return arr[0]
}
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4])) // 4
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = str => str.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three'])) // THREE
模拟实现flowRight:
// function compose (...args) {
// return function (value) {
// return args.reverse().reduce(function (acc, fn) {
// return fn(acc)
// }, value)
// }
// }
// 将上面的写法修改成箭头函数
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = str => str.toUpperCase()
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'two', 'three'])) // THREE
我们既可以把g和h组合,还可以把f和g组合,结果都是一样的
const _ = require('lodash')
const f = _.flowRight(_.toUpper, _.first, _.reverse)
console.log(f(['one', 'two', 'three'])) // THREE
const f2 = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
console.log(f2(['one', 'two', 'three'])) // THREE
const f3 = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f3(['one', 'two', 'three'])) // THREE
// NEVER SAY DIE --> never-say-die
const _ = require('lodash')
const split = _.curry((sep, str)=>_.split(str, sep))
// 为什么要调换两个参数的位置?因为要保证函数只有一个参数的函数,那就要通过柯里化实现。
// 而柯里化想要保留一个参数,那就只能保留最后一个参数,所以要把str放到最后
const join = _.curry((sep, arr) => _.join(arr, sep))
const map = _.curry((fn, arr) => _.map(arr, fn))
const log = v => {
console.log(v)
return v
}
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// const f = _.flowRight(join('-'), log, _.toLower, split(' ')) // n-e-v-e-r-,-s-a-y-,-d-i-e
// const f = _.flowRight(join('-'), log, split(' '), _.toLower) // never-say-die
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' ')) // never-say-die
console.log(f('NEVER SAY DIE'))
// lodash的fp模块
// NEVER SAY DIE --> never-say-die
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE')) // never-say-die
lodash与lodash-fp中map的区别
const _ = require('lodash')
const fp = require('lodash/fp')
// lodash中的map中的函数的参数有三个:(item, index, array)
console.log(_.map(['23', '8', '10'], parseInt)) // [ 23, NaN, 2 ]
// parseInt('23', 0, array) 第二个参数是0,则是10进制
// parseInt('8', 1, array) 第二个参数是1,不合法,输出NaN
// parseInt('10', 2, array) 第二个参数是2,表示2进制,输出2
// lodashFp中的map中的函数的参数有1个:(item)
console.log(fp.map(parseInt, ['23', '8', '10'])) // [ 23, 8, 10 ]
我们可以在数据处理的过程中定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
// point free 函数的组合
// Hello World => hello_world
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World')) // hello_world
案例:把一个字符串中的首字母提取并转换成大写,使用. 作为分隔符
// world wild web ==> W. W. W
const fp = require('lodash/fp')
// const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web')) // W. W. W
class Container {
constructor(value) {
this._value = value
}
map (fn) {
return new Container(fn(this._value))
}
}
let r = new Container(5)
.map(x=>x+1)
.map(x=>x*x)
// .map(console.log)
console.log(r) // Container { _value: 36 }
使用静态方法创建对象:
class Container {
static of (value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
let r = Container.of(5)
.map(x => x+2)
.map(x => x*x)
console.log(r) // Container { _value: 49 }
普通函子出现异常会变得不纯,MayBe可以处理异常值
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
let r = MayBe.of('hello')
.map(x => x.toUpperCase())
console.log(r) // MayBe { _value: 'HELLO' }
let r2 = MayBe.of(null)
.map(x => x.toUpperCase())
console.log(r2) // MayBe { _value: null }
let r3 = MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r3) // MayBe { _value: null } 无法知道null是哪里发生的
Left存异常信息,Right存正常信息
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))
}
}
let r1 = Right.of(12)
.map(x => x + 2)
console.log(r1)
let r2 = Left.of(12) // Right { _value: 14 }
.map(x => x + 2)
console.log(r2) // Left { _value: 12 }
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({error: e.message})
}
}
let r3 = parseJSON('{name: jal}')
console.log(r3) // Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
let r4 = parseJSON('{"name": "jal"}')
console.log(r4) // Right { _value: { name: 'jal' } }
let r5 = r4.map(x => x.name.toUpperCase())
console.log(r5) // Right { _value: 'JAL' }
const fp = require('lodash')
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))
}
}
const r = IO.of(process).map(p => p.execPath)
console.log(r) // IO { _value: [Function (anonymous)] }
console.log(r._value()) // /usr/local/Cellar/node/13.6.0/bin/node
Task函子处理异步执行
// folktale的使用
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
// curry 第一个参数写上参数的个数
const f = curry(2, (x, y) => x + y)
console.log(f(1, 2)) // 3
console.log(f(1)(2)) // 3
// folktale中的compose相当于lodash中的flowRight
const f2 = compose(toUpper, first)
console.log(f2(['one', 'two'])) // ONE
// Task 处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
// 柯里化的方法:split、find
const { split, find } = require('lodash/fp')
function readFile (filename) {
// 返回一个函子
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) {
resolver.reject(err)
}
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value) // "version": "1.0.0",
}
})
class Container {
static of (value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
map (fn) {
return Container.of(fn(this._value))
}
}
const fp = require('lodash')
const fs = require('fs')
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()
}
// 当fn返回一个函子的时候,用flatMap拍平
flatMap (fn) {
return this.map(fn).join()
}
}
const readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
const print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
// const cat = fp.flowRight(print, readFile)
// // IO(IO(x))
// // const r = cat('package.json')._value() // IO { _value: [Function (anonymous)] }
// const r = cat('package.json')._value()._value()
const r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r)
文章内容输出来源:拉勾教育大前端高薪训练营