技术点:JavaScript的几种常用高阶函数及其应用场景

JavaScript 惰性函数

需求

我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。

解决一:普通方法

let t
function foo() {
  if (t) return t
  t = new Date()
  return t
}

问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。

解决二:闭包

我们很容易想到用闭包避免污染全局变量。

let foo = (function () {
  let t
  return function () {
    if (t) return t
    t = new Date()
    return t
  }
})()

然而还是没有解决调用时都必须进行一次判断的问题。

解决三:函数对象

函数也是一种对象,利用这个特性,我们也可以解决这个问题。

function foo() {
  if (foo.t) return foo.t
  foo.t = new Date()
  return foo.t
}

依旧没有解决调用时都必须进行一次判断的问题。

解决四:惰性函数

不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。

let foo = function () {
  let t = new Date()
  foo = function () {
    return t
  }
  return foo()
}

应用场景

DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断:

// 简化写法
function addEvent(type, el, fn) {
  if (window.addEventListener) {
    el.addEventListener(type, fn, false)
  } else if (window.attachEvent) {
    el.attachEvent('on' + type, fn)
  }
}

问题在于我们每当使用一次 addEvent 时都会进行一次判断。

利用惰性函数,我们可以这样做:

function addEvent(type, el, fn) {
  if (window.addEventListener) {
    addEvent = function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    addEvent = function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
}

当然我们也可以使用闭包的形式:

let addEvent = (function () {
  if (window.addEventListener) {
    return function (type, el, fn) {
      el.addEventListener(type, fn, false)
    }
  } else if (window.attachEvent) {
    return function (type, el, fn) {
      el.attachEvent('on' + type, fn)
    }
  }
})()

当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数。

JavaScript 偏函数

定义

维基百科中对偏函数 (Partial application) 的定义为:

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

翻译成中文:

在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。

什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。

举个简单的例子:

function add(a, b) {
  return a + b
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 partial 函数可以做到局部应用
let addOne = partial(add, 1)

addOne(2) // 3

柯里化与局部应用

如果看过柯里化函数,实际上你会发现这个例子和柯里化很像,所以两者到底是有什么区别?

其实也很明显:

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

应用场景

比如封装 axios,则是使用了偏函数,可以在你调用之前先将接口地址进行传入,做好准备工作,然后在调用时只用传递参数

// http.ts
import Axios from 'axios'

const client = Axios.create({
  baseURL: '/api',
  validateStatus(status) {
    return status < 500
  },
})

export const http = {
  get: (url: string) => {
    const requestFn = (params?: object, config?: object): any =>
      client.get(url, { params, ...config })
    requestFn.path = url
    return requestFn
  },
  delete: (url: string) => {
    const requestFn = (params?: object, config?: object): any =>
      client.delete(url, { params, ...config })
    requestFn.path = url
    return requestFn
  },
  post: (url: string) => {
    const requestFn = (data: object, config?: object): any => client.post(url, data, config)
    requestFn.path = url
    return requestFn
  },
  put: (url: string) => {
    const requestFn = (data: object, config?: object): any => client.put(url, data, config)
    requestFn.path = url
    return requestFn
  },
}
// api.ts
import { http } from './http'
const { get, post, put,delete } = http

export const personApi = {
  usertoken: get('/auth/v1/user/token'), // 用户个人详情
  editName: put('/auth/v1/user/username'),
  editTel: put('/auth/v1/user/tel'),
  editPass: put('/auth/v1/user/password'),
  editEmail: put('/auth/v1/user/bind-email'),
  checkTel: post('/auth/v1/user/check-unbind'),
  checkEmail: post('/auth/v1/user/check-mail'),
  sendMail: post('/auth/v1/user/send-mail')
}
import { http } from './api'

const res = usertoken({ params1: 'xxx', params2: 'xxx' })

JavaScript 函数记忆

定义

函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。

举个例子:

function add(a, b) {
  return a + b
}

// 假设 memorize 可以实现函数记忆
let memoizedAdd = memorize(add)

memoizedAdd(1, 2) // 3
memoizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次

原理

实现这样一个 memorize 函数很简单,原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。

第一版

我们来写一版:

// 第一版 (来自《JavaScript权威指南》)
function memoize(f) {
  let cache = {}
  return function () {
    let key = arguments.length + Array.prototype.join.call(arguments, ',')
    if (key in cache) {
      return cache[key]
    } else return (cache[key] = f.apply(this, arguments))
  }
}

我们来测试一下:

let add = function (a, b, c) {
  return a + b + c
}

let memoizedAdd = memorize(add)

console.time('use memorize')
for (let i = 0; i < 100000; i++) {
  memoizedAdd(1, 2, 3)
}
console.timeEnd('use memorize')

console.time('not use memorize')
for (let i = 0; i < 100000; i++) {
  add(1, 2, 3)
}
console.timeEnd('not use memorize')

在 Chrome 中,使用 memorize 大约耗时 60ms,如果我们不使用函数记忆,大约耗时 1.3 ms 左右。

注意

什么,我们使用了看似高大上的函数记忆,结果却更加耗时,这个例子近乎有 60 倍呢!

所以,函数记忆也并不是万能的,你看这个简单的场景,其实并不适合用函数记忆。

需要注意的是,函数记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端 JavaScript 中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的。

第二版

因为第一版使用了 join 方法,我们很容易想到当参数是对象的时候,就会自动调用 toString 方法转换成 [Object object],再拼接字符串作为 key 值。我们写个 demo 验证一下这个问题:

let propValue = function (obj) {
  return obj.value
}

let memoizedAdd = memorize(propValue)

console.log(memoizedAdd({ value: 1 })) // 1
console.log(memoizedAdd({ value: 2 })) // 1

两者都返回了 1,显然是有问题的,所以我们看看 underscore 的 memoize 函数是如何实现的:

// 第二版 (来自 underscore 的实现)
let memorize = function (func, hasher) {
  let memoize = function (key) {
    let cache = memoize.cache
    let address = '' + (hasher ? hasher.apply(this, arguments) : key)
    if (!cache[address]) {
      cache[address] = func.apply(this, arguments)
    }
    return cache[address]
  }
  memoize.cache = {}
  return memoize
}

从这个实现可以看出,underscore 默认使用 function 的第一个参数作为 key,所以如果直接使用

let add = function (a, b, c) {
  return a + b + c
}

let memoizedAdd = memorize(add)

memoizedAdd(1, 2, 3) // 6
memoizedAdd(1, 2, 4) // 6

肯定是有问题的,如果要支持多参数,我们就需要传入 hasher 函数,自定义存储的 key 值。所以我们考虑使用 JSON.stringify:

let memoizedAdd = memorize(add, function () {
  let args = Array.prototype.slice.call(arguments)
  return JSON.stringify(args)
})

console.log(memoizedAdd(1, 2, 3)) // 6
console.log(memoizedAdd(1, 2, 4)) // 7

如果使用 JSON.stringify,参数是对象的问题也可以得到解决,因为存储的是对象序列化后的字符串。

适用场景

我们以斐波那契数列为例:

let count = 0
let fibonacci = function (n) {
  count++
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}
for (let i = 0; i <= 10; i++) {
  fibonacci(i)
}

console.log(count) // 453

我们会发现最后的 count 数为 453,也就是说 fibonacci 函数被调用了 453 次!也许你会想,我只是循环到了 10,为什么就被调用了这么多次,所以我们来具体分析下:

当执行 fib(0) 时,调用 1 次

当执行 fib(1) 时,调用 1 次

当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3 次

当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5 次

当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9 次

当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15 次

当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25 次

当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41 次

当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67 次

当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109 次

当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177

所以执行的总次数为:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次!

如果我们使用函数记忆呢?

let count = 0
let fibonacci = function (n) {
  count++
  return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}

fibonacci = memorize(fibonacci)

for (let i = 0; i <= 10; i++) {
  fibonacci(i)
}

console.log(count) // 12

我们会发现最后的总次数为 12 次,因为使用了函数记忆,调用次数从 453 次降低为了 12 次!

兴奋的同时不要忘记思考:为什么会是 12 次呢?

从 0 到 10 的结果各储存一遍,应该是 11 次呐?咦,那多出来的一次是从哪里来的?

所以我们还需要认真看下我们的写法,在我们的写法中,其实我们用生成的 fibonacci 函数覆盖了原本了 fibonacci 函数,当我们执行 fibonacci(0) 时,执行一次函数,cache 为 {0: 0},但是当我们执行 fibonacci(2) 的时候,执行 fibonacci(1) + fibonacci(0),因为 fibonacci(0) 的值为 0,!cache[address] 的结果为 true,又会执行一次 fibonacci 函数。原来,多出来的那一次是在这里!

多说一句

也许你会觉得在日常开发中又用不到 fibonacci,这个例子感觉实用价值不高呐,其实,这个例子是用来表明一种使用的场景,也就是如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆。而这种场景,当你遇到的时候,你就会知道的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: JavaScript 中的函数是指可以接受函数作为参数或返回函数作为返回值的函数。它可以帮助我们实现抽象、灵活地处理函数,是函数式编程的重要工具。 常见的 JavaScript 函数包括 map、filter、reduce 等,这些函数都可以对数组进行操作,并且可以传入一个函数作为参数来定制具体的操作方式。 例如,我们可以使用 map 函数将一个数组中的所有元素都转换成另一种形式: ``` const numbers = [1, 2, 3, 4, 5]; const squared = numbers.map(n => n * n); // squared is now [1, 4, 9, 16, 25] ``` 我们也可以使用 filter 函数过滤数组中的元素: ``` const numbers = [1, 2, 3, 4, 5]; const evens = numbers.filter(n => n % 2 === 0); // evens is now [2, 4] ``` 使用 reduce 函数可以对数组中的所有元素进行累积操作: ``` const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((accumulator, n) => accumulator + n, 0); // sum is now 15 ``` 这些函数都是函数,因为它们可以接受函数作为参数,并且本身也是函数函数的使用可以帮助我们写出更加简洁、抽象的代码,是函数式编程的重要工具。 ### 回答2: JavaScript 函数是指能够接收函数作为参数,并且返回一个新函数函数函数JavaScript中的一种特性,它的存在使得我们可以更灵活地处理函数,并使代码更简洁、模块化。 函数应用场景有很多,其中之一是函数的装饰器。我们可以使用一个函数来包装一个函数,以添加额外的功能或修改函数的行为,而无需修改原始函数的代码。这在某些情况下非常实用,例如我们可以用函数函数添加性能监测、日志输出等功能。 除了函数的装饰器,函数还可以用于函数的柯里化。柯里化是指将多个参数的函数转化为一个参数的函数。通过使用函数,我们可以将一个多参数函数转化为一个只接收部分参数的函数,并返回一个新函数来处理剩余的参数。这样做的好处是可以更方便地复用函数,同时也使代码更加清晰。 函数还可以用于函数组合。函数组合是指将多个函数组合在一起,构成一个新的函数。通过使用函数,我们可以将多个函数按照特定的顺序组合起来,并返回一个新的函数来处理输入。这种方式可以使得代码更加模块化、易于维护和测试。 总之,JavaScript函数是一种非常有用的特性,它可以让我们更灵活地处理函数,并使代码更加简洁和模块化。在实际开发中,熟练掌握函数应用,可以提代码的可读性、可维护性和可复用性。 ### 回答3: JavaScript函数是指能够接受函数作为参数,或者返回一个函数函数。在JavaScript中,函数可以被当作一等公民,就像其他数据类型一样,可以被赋值给变量,作为参数传递给其他函数,或者作为其他函数的返回值。 函数的优点之一是,它们可以更灵活地处理代码逻辑。通过将函数作为参数传递给函数,我们可以将代码逻辑抽象出来,减少代码的重复性,提代码的可维护性和复用性。例如,我们可以编写一个函数来实现数组的map、filter、reduce等操作,而不必重复编写循环和条件语句的代码。 另一个优点是,函数可以实现函数的柯里化(currying)。柯里化是一种将多参数函数转换为一系列单参数函数技术。通过将函数转换为柯里化的形式,我们可以更方便地进行函数组合、部分应用和延迟求值等操作。 除此之外,函数还可以用于实现函数的装饰器(decorator)。装饰器是一种在函数被调用之前或之后执行一些额外逻辑的方法。通过将函数作为参数传递给装饰器函数,我们可以在不修改原函数的情况下,给函数添加日志记录、性能分析、异常处理等功能。 总而言之,JavaScript函数是一种非常有用和强大的特性,它能够提代码的可读性、可维护性和复用性,同时也可以用于实现函数的柯里化和装饰器等功能。因此,掌握和运用函数JavaScript开发中的重要技能之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

易风有点疯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值