文章内容输出来源:拉勾教育大前端高薪训练营
文章说明:文章内容为学习笔记,学徒之心,仅为分享; 如若有误,请在评论区指出,如若您觉得文章内容对您有一点点帮助,请点赞、关注、评论,共享
下一篇:简单理解 Promise 的实现原理+自己手写一个 Promise
- 一、js 的函数式编程
- 二、函数是一等公民
- 三、js 中的常用高阶函数
- 四、js 中闭包
- 五、js 中的纯函数
- 六、函数的柯里化 (Haskell Brooks Curry)
- 七、函数组合(compose)
- 八、Functor (函子)
一、js 的函数式编程
1、什么是函数式编程?
函数式编程,缩写FP,是一种编程范式,也是一种编程风格,和面向对象是并列的关系。函数式编程我们可以认为是一种思维模式,加上实现方法。其思维模式就是把现实世界事物和事物之间的联系抽象到程序世界(是对运算过程进行抽象)
2、常见的编程方式?
(1)面向过程编程 、(2)面向对象编程 、 (3)函数式编程
(1)面向过程编程:必须按照步骤一步一步的执行,直到完成相应的功能,期间每一步都不能落下
(2)面向对象编程:把现实中的事物抽象成程序世界中的类和对象,通过封装、继承、多态来演示事物之间的关系
(3)函数式编程:把现实世界的事物和事物之间的联系抽象到程序的世界
2、为什么要学函数式编程?
- 函数式编程是随着React的流行收到越来越多的关注(React的高阶组件使用了高阶函数来实现,高阶函数就是函数式编程的一个特性,Redux也使用了函数式编程的思想)
- Vue3也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程中可以更好的利用tree shaking过滤无用代码
- 方便测试、方便并行处理
- 有很多库可以帮助我们进行函数式编程:lodash、underscore、ramda...
3、对于函数式编程思维方式的理解
- 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
- x -> f (联系、映射)->y ,y = f(x) 数学中运算
- 函数式编程中的函数指的是数学中的函数即映射关系 例如 y = sin(x),x 和 y 的关系
- 函数式编程用来描述数据(函数)之间的映射
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程,可以让代码进行复用
代码如下:
// 面向过程编程
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)
// 函数式编程
function add(num1, num2) {
return num1 + num2
}
let res = add (1,2)
console.log(res)
二、函数是一等公民
1、函数可以存储在变量中
let fn = function(){
console.log('函数可以存储在变量中,也就是匿名函数')
}
// 调用函数fn
fn()
2、函数可以作为参数 -> 也就是高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
- 注:高阶函数存在的意义:是用来抽象通用的问题
函数作为参数
// 模拟js中的forEach方法
function forEach(array, fun) {
for (let index = 0; index < array.length; index++) {
fun(array[index]) // 就是下方forEach()方法内的函数
}
}
// 测试
let arr = [1, 2, 3, 4, 5]
forEach(arr, function(item) {
console.log(item) // 输出 1,2,3,4,5
})
// 模拟js中的filtr方法
function filter(array, fun) {
const newArr = []
for (let index = 0; index < array.length; index++) {
if (fun(array[index])) {
// fun(array[index])就是下方的filter()方法里面的函数
newArr.push(array[index])
}
}
return newArr
}
// 测试
let arr = [1, 2, 3, 4, 5]
const data = filter(arr, function(item) {
return item > 2
})
console.log(data)
3、函数可以作为返回值
函数作为返回值
function fun() {
const msg = 'hello world'
return function() {
console.log(msg)
}
}
// 写法一:
const fn = fun()
fn() // hello world
//写法二:
fun()()
// 模拟js中的once方法,只执行一次
function once(fun) {
let tag = false
return function() {
if (!tag) {
tag = true // 函数once执行一次后,就不在执行的标记
return fun.apply(this, arguments) //必须是arguments才行
}
}
}
// 测试
const pay = once(function(money) {
console.log(`金额为${money}元`)
})
pay(5)
pay(10)
pay(15)
// 输出: 金额为5元
三、 js 中的常用高阶函数
forEach、map、filter、every、some、find、findIndex、reduce、sort ...
// 模拟js中的map方法
function map(array, fun) {
let newArr = []
for (let index = 0; index < array.length; index++) {
let rest = fun(array[index])
newArr.push(rest)
}
return newArr
}
// 测试
const arr = [1, 2, 3, 4, 5]
const newMap = map(arr, item => {
return item + 2
})
console.log(newMap) // [ 3, 4, 5, 6, 7 ]
// 模拟js中的every方法
function every(array, fun) {
let result = true
for (let index = 0; index < array.length; index++) {
result = fun(array[index])
if (!result) {
return [result, index]
}
}
return result
}
const arr = [30, 2, 3, 4, 5]
const newEvery = every(arr, item => {
return item > 18
})
console.log(newEvery) // [ false, 1 ]
// 模拟js中的some方法
function some(array, fun) {
let result = false
for (let index = 0; index < array.length; index++) {
result = fun(array[index])
if (result) {
return [result, array[index]]
}
}
return result
}
// 测试
const arr = [10, 2, 3, 4, 5]
const newSome = some(arr, item => {
return item > 18
})
console.log(newSome) // false
四、js 中的闭包
1、什么是闭包:
- 函数内嵌套函数
- 内部函数可以引用外部函数的成员(变量)
2、闭包作用:
- 延长外部函数中,内部成员(变量)的使用期限
3、闭包的本质:
- 函数在执行的时候会放到一个执行栈上,当函数执行完毕后,会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
简单的例子:
let fn = function() {
let str = '你好'
return function() {
console.log(str + '闭包')
}
}
const fun = fn()
fun() // 你好闭包
简化 js 中 Math.pow(),求数值的几次幂 -->闭包
const fn = function(mi) {
return function(number) {
return Math.pow(number, mi)
}
}
let mi2 = fn(2) // 2次幂
let mi3 = fn(3) // 3次幂
console.log(mi2(2)) // 2的2次幂 4
console.log(mi2(3)) // 2的3次幂 9
console.log(mi3(2)) // 2的3次幂 8
console.log(mi3(3)) // 3的3次幂 27
简单计算员工的工资 (基本工资+绩效)-->闭包
const getSalary = function(jb) {
return function(jx) {
return jx + jb //jx 绩效 jb 基本工资
}
}
let level1 = getSalary(12000) // 级别1的员工的基本工资
let level2 = getSalary(15000) // 级别2的员工的基本工资
console.log(level1(2000)) // 级别1的员工的总薪资,传入的绩效金额
console.log(level1(3000)) // 级别1的员工的总薪资,传入的绩效金额
console.log(level2(2000)) // 级别2的员工的总薪资,传入的绩效金额
console.log(level2(3000)) // 级别2的员工的总薪资,传入的绩效金额
五、js 中的纯函数
什么是纯函数?
- 相同的输入始终得到相同的输出,而且没有可观察到的副作用
- 纯函数就类似与数学中的函数,用来描述输入和输出之间的关系, 例如:y = f(x)
- lodash.js 库中的 FP 模块就是一个纯函数的代表
js数组中的slice方法就是纯函数
const arr = [1, 2, 3, 4, 5]
console.log(arr.slice(0, 3)) // [1, 2, 3]
console.log(arr.slice(0, 3)) // [1, 2, 3]
console.log(arr.slice(0, 3)) // [1, 2, 3]
// 相同的输入始终得到相同的输出,而且没有可观察到的副作用
js数组中的splice方法就是不纯的函数
const arr = [1, 2, 3, 4, 5]
console.log(arr.splice(0, 3)) // [1, 2, 3]
console.log(arr.splice(0, 3)) // [ 4, 5 ]
console.log(arr.splice(0, 3)) // []
// 相同的输入没有得到相同的输出
纯函数的好处?
- 可缓存:因为纯函数对相同的输入始终得到相同的输出,所以可以把纯函数的结果缓存起来
- 可测试
纯函数让测试更方便- 并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
// 计算圆的面积-带有缓存功能的
const _ = require('lodash')
const circle = function(r) {
console.log('只执行一次')
return Math.PI * r * r
}
const result = _.memoize(circle)
console.log(result(4))
console.log(result(4)) // 从缓存里面获取的结果
console.log(result(4)) // 从缓存里面获取的结果
// 打印结果:
// 只执行一次
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669
// 模拟lodash中的memoize()方法的功能
// 计算圆的面积-带有缓存功能的
const circle = function(r) {
console.log('只执行一次')
return Math.PI * r * r
}
function memoize(fun) {
let obj = {}
return function() {
let key = JSON.stringify(arguments)
obj[key] = obj[key] ? obj[key] : fun.apply(fun, arguments)
return obj[key]
}
}
let result = memoize(circle)
console.log(result(4))
console.log(result(4)) // 从缓存里面获取的结果
console.log(result(4)) // 从缓存里面获取的结果
// 打印结果:
// 只执行一次
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669
纯函数的副作用
- 副作用让一个函数变的不纯(如下例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部
的状态就无法保证输出相同,就会带来副作用- 副作用来源:
配置文件
数据库
获取用户的输入
……
// 不纯的
let mini = 18
function checkAge (age) {
return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge (age) {
let mini = 18
return age >= mini
}
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生
六、函数的柯里化 (Haskell Brooks Curry)
函数的柯里化
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余的参数,新的函数内返回相应的结果
// 使用柯里化解决上一个案例中硬编码的问题
function checkAge (age) {
let min = 18 // 硬编码
return age >= min
}
// 普通纯函数
function checkAge (min, age) {
return age >= min
}
checkAge(18, 24)
checkAge(18, 20)
checkAge(20, 30)
// 柯里化
function checkAge (min) {
return function (age) {
return age >= min
}
}
// ES6 写法
// let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
checkAge18(24)
checkAge18(20)
lodash 中的柯里化函数 _.curry(func)
- 功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提
供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。- 参数:需要柯里化的函数
- 返回值:柯里化后的函数
使用 lodash 中的柯里化函数的简单案例
const _ = require('lodash')
// lodash中的柯里化函数_.curry,返回的是柯里化函数,可以传递一个参数reg,也可以传递所有参数
const match = _.curry((reg, str) => {
return str.match(reg)
})
// 使用
const haveSpace = match(/\s+/g) // 匹配字符串中的所有空白字符
const haveNumber = match(/\d+/g) // 匹配字符串中的所有数字
// console.log(haveSpace('hello world')) // [ ' ' ]
// console.log(haveNumber('hello world123')) // [ '123' ]
// 过滤数组,找到数组中,带有空白字符的所有元素
const filter = _.curry((func, array) => {
return array.filter(func)
})
const arr = ['lebron james', 'kobe_Bryant']
// 可以直接传递所有参数
console.log(filter(haveSpace, arr))
// 可以传递一个参数,返回的是柯里化函数
const findSpace = filter(haveSpace)
console.log(findSpace(arr))
// 可以传递一个参数,返回的是柯里化函数
console.log(filter(haveSpace)(arr)) // 高阶函数
模拟实现 lodash 中的柯里化函数_.curry()
function curry(func) {
return function curriedFn(...args) {
// 判断实参和形参的个数
if (args.length < func.length) {
return function() {
return curriedFn(...args.concat(Array.from(arguments)))
}
}
// 实参和形参个数相同,调用 func,返回结果
return func(...args)
}
}
function getTotal(a, b, c) {
return a + b + c
}
// console.log(getTotal.length) // 参数的个数是3
const myCurry = curry(getTotal) // 返回的是一个柯里化函数
console.log(myCurry(1, 2, 3))
console.log(myCurry(1)(2, 3)) // 高阶函数,myCurry(1)返回的是一个函数,然后再调用这个返回的函数myCurry(1)(2, 3)
console.log(myCurry(1, 2)(3)) // 高阶函数,myCurry(1,2)返回的是一个函数,然后再调用这个返回的函数myCurry(1,2)(3)
总结
- 柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数,这是一种对函数参数的'缓存'
- 让函数变的更灵活,让函数的粒度更小
- 可以把多元函数(多个参数)转换成一元函数(一个参数),把柯里化函数 放在 组合函数内 可以产生强大的功能
七、函数组合(compose)
- 函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
组合函数演示
// 获取数组中的最后一个元素
// 函数组合的辅助函数
function reserve(arr) {
return arr.reverse()
}
// 函数组合的辅助函数
function first(arr) {
return arr[0]
}
// 组合函数,返回一个新函数
function compose(fun1, fun2) {
return function(val) {
return fun1(fun2(val))
}
}
const arr = [1, 10, 100, 1000, 10000]
const attr = compose(first, reserve) // 组合成新的函数
console.log(attr(arr))
lodash 中的组合函数
lodash 中组合函数 flow() 或者 flowRight(),他们都可以组合多个函数
flow() 是从左到右运行
flowRight() 是从右到左运行,使用的更多一些
// 把数组中的最后一个元素,变成大写
const _ = require('lodash')
const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const f = _.flowRight(toUpper, first, reverse) // 返回的是一个新的函数
console.log(f(['one', 'two', 'three']))
模拟实现 lodash 的 flowRight 方法
// 多函数组合
function compose(...fns) {
return function(value) {
return fns.reverse().reduce(function(acc, fn) {
return fn(acc)
}, value) // acc 的初始值 value
}
}
// ES6
const compose = (...fns) => value =>
fns.reverse().reduce((acc, fn) => fn(acc), value)
函数的组合要满足结合律 (associativity):
我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
// 结合律(associativity)
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
lodash/fp 模块的练习
// 把一个字符串中的首字母找到,并转换成大写,使用.作为分隔符
// 'hello world you' ==> 'H.W.Y'
const fp = require('lodash/fp')
//用到lodash的中的组合函数 ,有2次循环遍历
// const changeToUP = fp.flowRight(
// fp.join('.'),
// fp.map(fp.first),
// fp.map(fp.toUpper),
// fp.split(' ')
// )
//用到lodash的中的组合函数 ,只有1次循环遍历
const changeToUP = fp.flowRight(
fp.join('.'),
fp.map(fp.flowRight(fp.first, fp.toUpper)),
fp.split(' ')
)
console.log(changeToUP('hello world you'))
八、Functor (函子)
1-什么是 Functor (函子)
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
2-为什么要学函子
函子(representative functor)是范畴论里的概念,指从任意范畴到集合范畴的一种特殊函子
我们没有办法彻底解决函数的副作用,但是我们可以尽可能的将副作用控制在可控的范围内,我们可以通过函子去处理副作用,我们也可以通过函子去处理异常,异步操作等
3-基本的函子Functor (函子)
// 一个容器,包裹一个值 ==>基本的函子
class Container {
// of 静态方法,可以省略 new 关键字创建对象
static of(value) {
return new Container(value)
}
constructor(value) {
this._value = value
}
// map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
map(fn) {
// 返回新的函子
return Container.of(fn(this._value))
}
}
// 测试
const r = Container.of(3)
.map(x => x + 2)
.map(x => x * x)
console.log(r) // Container { _value: 25 }
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象 (每一个函子里面都有一个 map 方法,供外部调用)
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的盒子(函子)
在 Functor 中如果我们传入 null 或 undefined,会报错
// 值如果不小心传入了空值(副作用)
Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
为了解决上述的副作用,需要用到 maybe 函子
4-MayBe 函子
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe {
static of(value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
// 如果对空值变形的话直接返回 值为 null 的函子
map(fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
// 辅助函数,用来判断传入的值是否为null或undefined
isNothing() {
return this._value === null || this._value === undefined
}
}
// 传入具体值
const result = MayBe.of('Hello World').map(x => x.toUpperCase())
console.log(result) // MayBe { _value: 'HELLO WORLD' }
// 传入 null 的情况
const result1 = MayBe.of(null).map(x => x.toUpperCase())
console.log(result1) // => MayBe { _value: null }
但是在 MayBe 函子中,我们很难确认是哪一步产生的空值问题,如下例:
MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
// => MayBe { _value: null }
所以需要用到 Either 函子
- Either 两者中的任何一个,类似于 if...else...的处理
- 异常会让函数变的不纯,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 })
}
}
let r = parseJSON('{ "name": "zs" }').map(x => x.name.toUpperCase())
console.log(r)
5- IO 函子
- IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
- IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
- 把不纯的操作交给调用者来处理
// 用到了lodash/fp模块
const fp = require('lodash/fp')
class IO {
static of(x) {
return new IO(function() {
return x
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
// 把当前的 value 和 传入的 fn 组合成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用
let io = IO.of(process).map(p => p.execPath)
console.log(io._value())
但是在使用 IO 函子的时候,有一个问题,如下面的代码
const fs = require('fs')
const fp = require('lodash/fp')
class IO {
static of(x) {
return new IO(function() {
return x
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
// 把当前的 value 和 传入的 fn 组合成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
// 读取文件
let readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8')
})
}
// 打印文件
let print = function(x) {
return new IO(function() {
console.log(x) // x是readFile()的返回值 IO函子
return x
})
}
// 把2个函数合并成一个新的函数 fp.flowRight(),默认从右像左执行
let cat = fp.flowRight(print, readFile)
// cat函数里面2个函数的大致执行顺序:
// 把readFile()的返回值 IO函子,也返回出去。返回给print()
// 相当于print()函数,返回了2个IO函子(一个IO函子里面套一个IO函子)
// 也就是cat的值是2个函子:IO(IO(x))
// 里面的IO函子是readFile()的返回值 IO函子
// 外面的IO函子是print()的返回值 IO函子
// 调用
let r = cat('package.json')
._value() // readFile()的返回值 IO函子
._value() // 读取出来的文件内容
console.log(r)
上面的的代码,可以看出 IO 函子进行了嵌套,我们在获取想要得到的值的时候,就会有些麻烦,一直在调用_value()方法,
_value()方法调用多了之后,代码就不便于阅读,所以为了解决该问题,可以使用 Monad(单子)来处理
6-Monad(单子)
-
Monad 函子是可以变扁的 Pointed 函子
Pointed 函子:
Pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)Pointed 函子的简单例子:
class Container { static of (value) { return new Container(value) } … … } Contanier.of(2) .map(x => x + 5)
一个函子如果具有 join 和 of 两个方法 并遵守一些定律就是一个 Monad
// 对上面的IO函子的处理,解决函子的嵌套问题
const fs = require('fs')
const fp = require('lodash/fp')
// IO Monad
class IO {
static of(x) {
return new IO(function() {
return x
})
}
constructor(fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
join() {
return this._value()
}
flatMap(fn) {
return this.map(fn).join()
}
}
// 读取文件
let readFile = function(filename) {
return new IO(function() {
return fs.readFileSync(filename, 'utf-8')
})
}
// 打印文件
let print = function(x) {
return new IO(function() {
console.log(x) // x是readFile()的返回值 IO函子
return x
})
}
// package.json 是一个json文件,里面是json数据
let r = readFile('package.json') //
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r)
// 代码运行在node环境下,步骤如下:
// 安装node
// 新建一个文件夹/目录
// 在文件夹/目录内,打开命令行工具,使用 npm init 初始化一个package.json文件
// 安装lodash,在文件夹/目录内,打开命令行工具,运行 : npm install --save lodash
// 在文件夹/目录内,新建一个文件index.js
// 把上面的代码复制到index.js 内
// 在命令行工具内,运行 node index.js 即可
结语:
来拉勾教育训练营学习已经有一周了,在没有来之前,我都是回家之后打游戏(游戏名:斗战神),来到这里之后才发现居然还有很多大佬也在学习,真的很惊讶,本人自身水平垃圾的一批,再不学习,以后可能一直就是混吃等死的状态了
- 首先来说,拉钩的课程很干,每个视频很短,都是干货,讲师没有一句废话,视频内容覆盖比较广,布置的作业也比较符合实际,导师也会及时批改,然后一周或两周必有直播,直播都会回答学习过程中所遇到的问题和新的内容
- 其次来说,每个班都有班级群,群里居然还有5年或6年的前端开发的大佬(⊙▽⊙); 班主任和导师也在群里,有任何问题都可以在群里@导师,班级群真的很活跃
- 最后来说一句,如果有其他人也是在打游戏混日子,不如来拉勾教育训练营,尝试着改变一下自己目前所处的环境