JavaScript 深度剖析

文章内容输出来源:拉勾教育大前端高薪训练营
文章说明:文章内容为学习笔记,学徒之心,仅为分享; 如若有误,请在评论区指出,如若您觉得文章内容对您有一点点帮助,请点赞、关注、评论,共享

下一篇:简单理解 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年的前端开发的大佬(⊙▽⊙); 班主任和导师也在群里,有任何问题都可以在群里@导师,班级群真的很活跃
  • 最后来说一句,如果有其他人也是在打游戏混日子,不如来拉勾教育训练营,尝试着改变一下自己目前所处的环境

你可能感兴趣的:(JavaScript 深度剖析)