Skip to content

补充一

函数式编程

重点掌握:

  • 函数式编程的核心思想

  • 纯函数

    在程序设计中,若一个函数符合以下要求,则它可能被认为是 纯函数

    • 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或 状态 无关,也和 I/O 设备产生的外部输出无关
    • 该函数不能有语义上可观察的函数副作用,诸如:"触发事件",使输出设备输出,或更改输出值以外物件的内容等(如果参数是引用传递,对参数的更改会影响函数以外的数据,因此不是纯函数)
  • 柯里化

  • 函数组合 lodash/fpcompose(fn, fn1)flowRight 的别名

  • 函子可以作为了解 Array.of arr.map

函数式编程是一种编程范式,和面向对象编程时并列关系(编程范式:思想 + 实现的方式)

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

柯里化:把多个参数的函数转换可以具有任意个参数的函数,可以给函数组合提供细粒度的函数

  • Vue.js 源码中使用柯里化

    src/platform/web/patch.js

    js
    function createPatch(obj) {
      return function patch(vdom1, vdom2) {}
    }
    const patch = createPatch(...)
  • 固定不常变化的参数

    js
    function isType(type) {
      return function (obj) {
        return Object.prototype.toString.call(obj) === `[object ${type}]`
      }
    }
    const isObject = isType('Object')
  • 延迟执行(模拟 bind 方法)

    js
    Function.prototype.myBind = function (context, ...args) {
      return (...rest) => this.call(context, ...args, ...rest)
    }

模拟 call

js
Function.prototype.myCall = function (context, ...rest) {
  const key = Symbol('KEY')
  context[key] = this
  const result = context[key](...rest)
  delete context[key]
  return result
}

函子使用场景:

  • 作用是控制副作用(IO)、异常处理(Either)、异步任务(Task)
  • folktale
js
class Functor {
  static of(value) {
    return new Functor(value)
  }
  constructor(value) {
    this._value = value
  }
  map(fn) {
    return Functor.of(fn(this._value))
  }
  value(f) {
    return f(this._value)
  }
}

函数执行上下文与闭包

执行上下文(Execution Context)

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

函数执行阶段可以分为两个:函数建立阶段、函数执行阶段

  • 函数建立阶段:当调用函数时,还没有执行函数内部的代码

    创建执行上下文对象

    js
    fn.ExecutionContent = {
      variableObject: {}, // 函数中的 arguments、参数、局部成员
      scopeChains: {}, // 当前函数所在的父级作用域中的活动对象
      this: {}, // 当前函数内部的 this 指向
    }
  • 函数执行阶段

    js
    fn.ExecutionContent = {
      activationObject: {}, // 函数中的 arguments、参数、局部成员
      scopeChains: {}, // 当前函数所在的父级作用域中的活动对象
      this: {}, // 当前函数内部的 this 指向
    }

[[Scopes]] 作用域链,函数在创建时就会生成该属性,JS 引擎才可以访问。这个属性存储的是所有父级中的变量对象

  • Script
  • Global
  • Closure
js
function fn(a, b) {
  function inner() {
    console.log(a, b)
  }
  console.dir(inner)
}
console.dir(fn)
const f = fn(1, 2)

发生闭包的两个必要条件:

  1. 外部对一个函数 makeFn 内部有引用
  2. 在另一个作用域能访问到 makeFn 作用域内部的局部成员
js
function makeFn() {
  let name = 'MDN'
  return function inner() {
    console.log(name)
  }
}
let fn = makeFn()
fn()

闭包的好处:缓存参数

但是闭包会在外部对内部有引用,造成内存泄露,可以把 fn = null 取消引用

宏任务微任务

MDN Promise then

阮一峰 Promise resolve

当 Promise 变成接受状态(fulfilled)时调用的函数。该函数有一个参数,即接受的最终结果(the fulfillment value)。如果该参数不是函数,则会在内部被替换为 (x) => x,即原样返回 promise 最终结果的函数

js
Promise.resolve(1)
  // .then(x => x)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log) // 1

深入:微任务与Javascript运行时环境

每个代理都是由 事件循环 驱动的,事件循环负责收集用事件(包括用户事件以及其他非用户事件等)、对任务进行排队以便在合适的时候执行回调。然后它执行所有处于等待中的 JavaScript 任务(宏任务),然后是微任务,然后在开始下一次循环之前执行一些必要的渲染和绘制操

  • 看一段伪代码
js
// 事件循环, 主线程
while (macroQueue.waitForMessage()) {
  // 1. 执行完调用栈上当前的宏任务(同步任务)
  // call stack

  // 2. 遍历微任务队列,把微任务队里上的所有任务都执行完毕(清空微任务队列)
  // 微任务又可以往微任务队列中添加微任务
  for (let i = 0; i < microQueue.length; i++) {
    // 获取并执行下一个微任务(先进先出)
    microQueue[i].processNextMessage()
  }

  // 3. 渲染(渲染线程)

  // 4. 从宏任务队列中取 一个 任务,进入下一个消息循环
  macroQueue.processNextMessage()
}

任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要 在下一次迭代开始之后才会被执行
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务

产生宏任务的方式

  • script 代码块
  • setTimeout
  • setInterval
  • setImmediate(非标准,IE 和 Node 中支持)
  • 注册事件

产生微任务的方式

js
queueMicrotask(() => {
  console.log('微任务')
})

使用微任务最重要原因:

  • 减少操作中用户可感知到的延迟
  • 确保任务顺序的一致性,即便当结果或数据是同步可用的
  • 批量操作的优化

面试题

问题1

js
async function t1() {
  let a = await 'lagou'
  console.log(a)
}
t1()

await 是一个表达式,如果后面不是一个 promise 对象,就直接返回对应的值

js
function* t1() {
  let a = yield 'lagou'
  console.log(a)
}
co(t1)
function co(generator) {
  const g = generator()
  function handleResult(result) {
    if (result.done) {
      return Promise.resolve(result.value)
    }
    // 如果 yield 后面不是 Promise 对象,保证成 Promise 对象
    if (!(result.value instanceof Promise)) {
      result.value = Promise.resolve(result.value)
    }
    return result.value.then(function (data) {
      handleResult(g.next(data))
    })
  }
  return handleResult(g.next())
}

问题2

js
async function t2() {
  let a = await new Promise(resolve => {})
  console.log(a) // 没有输出
}
t2()

await 后面如果跟一个 promise 对象,await 将等待这个 promise 对象的 resolve 状态的值为 value,且将这个值返回给前面的变量,此时的 promise 对象的状态是一个 pending 状态,没有 resolve 状态值,所以什么也打印不了

js
function* t2() {
  let a = yield new Promise(resolve => {})
  console.log(a)
}
const generator = t2()
const result = generator.next()
result.value.then(v => {
  generator.next(v)
})

问题3

js
async function t3() {
  let a = await new Promise(resolve => {
    resolve()
  })
  console.log(a) // undefined
}
t3()

问题4

js
async function t4() {
  let a = await new Promise(resolve => {
    resolve('hello')
  })
  console.log(a) // hello
}
t4()

问题5

js
async function async1() {
  console.log('AAA')
  async2()
  console.log('BBB')
}
async function async2() {
  console.log('CCC')
}
console.log('DDD')
setTimeout(function () {
  console.log('FFF')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('GGG')
  resolve()
}).then(function () {
  console.log('HHH')
})
console.log('III')
/* 
DDD
AAA
CCC
BBB
GGG
III
HHH
FFF
*/

问题6

js
async function async1() {
  console.log('AAA')
  await async2()
  console.log('BBB')
}
async function async2() {
  console.log('CCC')
}
console.log('DDD')
setTimeout(function () {
  console.log('FFF')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('GGG')
  resolve()
}).then(function () {
  console.log('HHH')
})
console.log('III')
/* 
DDD
AAA
CCC
GGG
III
BBB
HHH
FFF
*/

常备不懈,才能有备无患