Skip to content

Article

JavaScript 函数式编程

函数式编程思想是一种对运算过程的抽象,这里的函数是指数学意义上映射关系(如y=sin(x)),也称为纯函数,而编程语言定义的函数除了运算,往往带有副作用,如 IO 操作、影响外部变量等。

1. 函数式编程基础

1.1 函数是一等公民

函数只是一个特殊的对象,可以用变量表示(函数是一等公民)

js
function add(x, y) {
  return x + y
}

const sum = add
sum(1, 2) // 3

1.2 高阶函数

  • 函数可以作为参数
  • 函数可以作为返回值

函数作为参数,模拟实现 forEach 函数:

js
function myForEach(array, f) {
  for (let i = 0; i < array.length; i++) {
    f(array[i])
  }
}

myForEach([1, 2, 3], (itemÏ) => {
  console.log(item)
})

函数作为返回值,封装一个 once 函数,生成一个只能执行一次的函数:

js
function once(fn) {
  let done = false
  return (...args) => {
    if (!done) {
      done = true
      return fn(...args)
    }
  }
}

const pay = once(function (money) {
  console.log(`消费了${money}`)
})

pay(5)
pay(5)
pay(5)

//
Article

JavaScript 异步编程

JavaScript的一个核心特性是单线程执行,这里的单线程是指代码的执行是单线程的(宿主环境的API可以多线程),其最初的目的是为了避免多线程的DOM操作可能带来的错乱。优点是更安全简单,缺点是当遇到一个耗时任务时,后面的任务会被阻塞,从而拖延整个程序的执行。

为了解决耗时任务阻塞线程的问题,JavaScript将任务的执行模式分为了同步模式和异步模式,运行环境也提供了以同步或者异步模式运行的API。

  • 同步模式: 任务排队执行,执行过程容易理解。
  • 异步模式: 对于耗时操作,开启过后就立即往后执行下一个任务,异步后续的逻辑一般通过回调函数的方式定义,在耗时任务执行过后就会自动执行传入的回调函数。

JavaScript引擎会先执行完调用栈中的任务,然后通过事件循环从“消息队列”中再取一个出来执行,在此过程中,可以随时再往消息队列中添加任务。也正是因为大量的异步模式的API导致代码一些复杂的异步逻辑不易阅读。

1. 回调函数

回调函数是所有异步编程方案的根基,回调函数由调用者定义,交给执行者执行,具体用法就是把函数作为参数传递罢了。

除了传递回调函数参数以外,还有几种常见的实现异步的方式如事件机制、发布订阅,可以理解为回调函数的变体。

当使用传统回调方式去完成复杂的异步流程时,会无法避免大量的回调函数嵌套,造成回调地狱问题

js
setTimeout(function () {
  console.log('1.执行任务1 -> 三秒后执行任务2')
  setTimeout(function () {
    console.log('2.执行任务2 -> 2秒后执行任务1')
    setTimeout(function () {
      console.log('3.执行任务3 -> 1秒后执行完毕')
      // 回调地狱...
    }, 1000)
  }, 2000)
}, 3000)

2. Promise

为了避免回调地狱问题,CommonJS社区提出了Promise规范,并在ES2015中被标准化。Promise实际上就是一个对象,用来表示一个异步任务执行过后究竟是成功还是失败,无论成功或失败只需要执行相应的回调函数即可。

2.1 Promise 的基本用法

  1. 构建Promise实例时传入一个函数,函数的参数为两个处理函数,在Promise里编写承诺的逻辑,即什么情况下成功,什么情况下失败,并在成功或失败时改变状态,执行提前注册的处理函数
  2. 使用then方法分别注册promise实例的onFulfilledon和onRejected回调函数
  3. 注意注册的函数不会立即执行,而是等待同步代码执行完才会执行(如下例)
js
let a = 1, b = 2

const promise = new Promise(function (res, rej) {
  if (a > b)
    res('success') // 成功时调用res函数
  else
    rej('fail')    // 失败时调用rej函数
})

promise.then(function (sucStr) {
  console.log(sucStr)
}, function (failStr) {
  console.log(failStr)
})

console.log('第一个同步任务执行才会执行提前注册的异步处理函数')

2.2 Promise 封装一个异步任务函数

js
// 使用promise封装一个ajax请求
function ajax(url) {
  return new Promise(function (resolve, reject) {
    let xhr = new XMLHttpRequest()
    xhr.open('GET', url)        // 指定请求方法和地址
    xhr.responseType = 'json'   // 指定相应类型为json
    xhr.onload = function () {  // 请求完成后执行onload函数
      if (this.status === 200) {
        resolve(this.response)
      }
      else {
        reject(new Error(this.status))
      }
      xhr.send()
    }
  })
}

ajax('/user.json').then(function (res) {
  console.log(res)
}, function (err) {
  console.log(err)
})

2.3 Promise 的链式调用

promise的本质也是使用回调函数,即通过then方法传递进去,而且promise将回调分成了两种即成功的回调和失败的回调。但是如果需要串联执行多个异步任务,还是会出现回调地狱的问题,这时候使用promise就没有任何意义了,还额外增加了复杂度。

所以嵌套使用promise是不可取的,正确的做法是使用promise方法链式调用的特点来尽量保证串联异步任务的 “扁平化”。

  • promise的then方法会返回一个全新的promise对象,用于实现promise的链式调用
  • 后面的then就是在为前面then返回的promise注册回调
  • 前面then方法中调用函数的返回值会作为后面then方法回调的参数
  • 若回调中返回的是一个promise对象,则后面then方法的回调会等待他的结束
js
ajax('api/url1')
	.then(value => {
		return ajax('api/url2')
  })
	.then(value => {
		return ajax('api/url3')
  })
	.then(value => {
		return ajax('api/url4')
  })
	.catch(error => {
		console.log(error)
  })

多次catch的链式调用(for test):

js
const p = Promise.reject(new Error('fail1'))

p.catch(err => console.log(err))
	.then(()=>console.log('ok1'))
	.then(()=>console.log('ok2'))
	.then(()=>{throw new Error('fail2')})
	.catch(err => console.log(err))
	.then(()=>console.log('ok3'))
	.then(()=>console.log('ok4'))

2.4 Promise 的异常处理

除了promise的执行逻辑执行了reject函数,在promise在执行的过程中出现错误或者主动抛出异常也会执行reject函数,所以then方法中注册的onrejected函数就是为promise的异常做处理,即promise失败或者出现异常都会被执行。其实onrejected函数的注册还有一个更常见的用法,即使用promise实例的catch方法注册onrejected回调

js
ajax('/user.json')
  .then(function onFulfilled(res) {
    console.log(res)
  })
  .catch(function onRejcted(error) {
    console.log(error)
  })

// 等同于
ajax('/user.json')
  .then(function onFulfilled(res) {
    console.log(res)
  })
  .then(undefined, function onRejcted(error) {
    console.log(error)
  })

用catch方法更常见,因为更适合链式调用。错误会随着promise链条传递,所以最后使用catch方法更像是给整个promise链条注册的失败回调

2.5 Promise 的静态方法

js
Promise.resolve('success')  // 直接返回一个成功的Promise对象
Promise.resolve(promise)    // 如果接收到一个promise对象则原样返回

Promise.reject(new Error('rejected')) // 快速得到一个一定是失败的promise对象
	.catch(error => console.log(err))

2.6 Promise 并行执行

前面都是多个promise串联执行异步任务,而当多个任务彼此没有依赖时可以并行执行来提高执行速度,Promise.all方法可以将多个promise合并为一个promise统一管理。Promise.all()接收一个数组,数组中每个元素都是一个promise对象,并返回一个全新的promise对象,当其内部所有的promise都完成后,这个新的promise对象才会完成,且其拿到的结果也是一个数组,数组包含每个异步任务执行的结果。其中任何一个promise失败,这个新的promise也就失败了。

示例:先拿到要访问的url列表,再并行访问

js
ajax('/urls.json')
  .then(value => {
    const urls = Object.values(value)
    const tasks = urls.map(map => ajax(url))
    return Promise.all(tasks)
  })
  .then(values => {
    console.log(values)
  })

Promise.race()也可以将多个promise合并为一个promise对象,但Promise.all()等待所有任务结束才会结束,而Promise.race()只会等待第一个先结束的任务。

示例:一个请求promise+一个定时promise用于处理请求超时

js
const request = ajax('/urls.json')
const timeout = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('timeout')), 500)
})

Promise.race([
  request,
  timeout
])
  .then(value => { console.log(value) })
  .catch(error => { console.log(error) })

2.7 Promise 的执行时序(宏任务&微任务)

即使Promise没有任何的异步操作,它的回调函数仍然不会立即执行而是进入回调队列中排队,即等待所有同步代码执行完后才会执行。

但在回调队列中排队等待的任务并不是平等的。回调队列中的任务称为宏任务,宏任务的执行过程中可以临时加上一些额外需求,这些临时需求可以作为新的宏任务进到队列排队(如下setTimeout回调),也可以作为当前任务的微任务,直接在本轮调用的末尾立即执行(如下Promise回调),微任务的概念是后来才提到JS中的,目的是为了提高整体响应能力。目前绝大多数异步调用的API都是作为宏任务执行,而作为微任务的有Promise & MutationObserver & Node中的process.nextTick

js
console.log('start')

setTimeout(() => console.log('settimeout'), 0)

Promise.resolve()
  .then(() => console.log('promise1'))
  .then(() => console.log('promise2'))
  .then(() => console.log('promise3'))

console.log('end')

//