Sat, 20 Jun 2020 07:51:54 +0000

ES6 的函数式 js —– 递归模式

原文地址:https://medium.com/dailyjs/functional-js-with-es6-recursive-patterns-b7d0813ef9e3

作者:Casey Morris

这是处理什么问题? 函数式编程正在兴起,这是一个令人非常兴奋的话题。它使我可以编写简洁的声明性代码,易于测试和推理。 什么是函数式编程? 我将把答案回答给对此主题有更多了解的人,Eric Elliot 说:

函数式编程(通常缩写为FP)是通过组合纯函数、避免共享状态、可变数据和副作用来构建软件的过程。函数式编程是声明式的,而不是命令式的,应用的状态通过纯函数传递。与面向对象编程相反,在面向对象编程中,应用的状态通常与对象中的方法共享和同步。

ES6带来了许多特性,让我们可以轻松编写纯函数,而 rest/spread 是最强大的功能之一。使用 rest 参数,我们可以使用递归进行无循环的循环。 在本文中,我们将重写许多支持函数模式的常用 JavaScript 方法/函数。

前言

以下函数用于演示和学习。下面的许多函数都是尾部递归的,应该进一步优化。优化尾部递归不是本文的主题。ES6带来了尾部调用优化,但必须与“严格模式”一起使用。

返回数组中的第一项。当您需要将第一项与数组的其余项分隔开时,非常有用。为此,我们使用了解构赋值。

const head = ([x]) => x

示例:

const array = [1,2,3,4,5]
head(array) // 1

Tail

返回数组中除第一项以外的所有项。

const tail = ([, ...xs]) => xs

上面的示例与如下所示本质上是一样的

const tail = ([x, ...xs]) => xs

因为我们不需要在结果中使用 x,所以我们可以删除它,但是保留逗号以获取数组中的其他项。

示例:

const array = [1,2,3,4,5]
tail(array) // [2,3,4,5]

Def

返回参数是否已定义。

const def = x => typeof x !== 'undefined'

示例:

const defined = 'this is defined'
def(defined) // true
def(doesntExist) // false

Undef

返回参数是否未定义。

const undef = x => !def(x)

示例:

const defined = 'this is defined'
undef(defined) // false
undef(doesntExist) // true

Copy

不使用 array .slice() 返回数组的拷贝。利用 spread。

const copy = array => [...array]

示例:

let array = [1,2,3,4,5]
let copied = copy(array)
copied.push(6)

array // [1,2,3,4,5]
copied // [1,2,3,4,5,6]

Length

返回数组的长度。这是使用递归循环遍历数组的一种非常简单的形式,尽管在本例中数组的值无关紧要(数组中的每一项都从 1 开始递增)。我们使用 len 参数来避免尾部递归。

const length = ([x, ...xs], len = 0) => def(x) ? length(xs, len + 1) : len

如果我们不关心尾部递归,我们可以把它写成

const length = ([x, ...xs]) => def(x) ? 1 + length(xs) : 0

这将为数组中的每个项添加一个堆栈帧,而避免尾部递归的版本将替换单个堆栈帧。如果传入的数组足够大,它将抛错 “Maximum call stack size exceeded”。

示例:

const array = [1,2,3,4,5]
length(array) // 5

Reverse

返回一个反向数组。

const reverse = ([x, ...xs]) => def(x) ? [...reverse(xs), x] : []

示例:

const array = [1,2,3,4,5]
reverse(array) // [5,4,3,2,1]

Array.reverse() 可以使用,但是它有一个副作用,它会把原来的数组改掉。以下所示:

const array = [1,2,3,4,5]

const newArray = array.reverse() // [5,4,3,2,1]
array // [5,4,3,2,1]

// using the reverse method we just created
const array2 = [1,2,3,4,5]

const newArray2 = reverse(array2) // [5,4,3,2,1]
array2 // [1,2,3,4,5]

First

返回一个新数组,其中包含给定数组的前 n 项。

const first = ([x, ...xs], n = 1) => def(x) && n ? [x, ...first(xs, n - 1)] : []

示例:

const array = [1,2,3,4,5]
first(array, 3) // [1,2,3]

Last

返回一个新数组,其中包含给定数组的最后 n 项。

const last = (xs, n = 1) => reverse(first(reverse(xs), n))

示例:

const array = [1,2,3,4,5]
last(array, 3) // [3,4,5]

slice

返回在给定索引处插入值的新数组。

const slice = ([x, ...xs], i, y, curr = 0) => def(x)
  ? curr === i
    ? [y, x, ...slice(xs, i, y, curr + 1)]
    : [x, ...slice(xs, i, y, curr + 1)]
  : []

示例:

const array = [1,2,4,5]
slice(array, 2, 3) // [1,2,3,4,5]

isArray

返回是否是数组。允许我们以更函数化的方式编写 Array.isArray()。

const isArray = x => Array.isArray(x)

示例:

const array = [1,2,3,4,5]
isArray(array) // true

Flatten

将多维数组变成一维数组。

const flatten = ([x, ...xs]) => def(x)
    ? isArray(x) ? [...flatten(x), ...flatten(xs)] : [x, ...flatten(xs)]
    : []

示例:

const array1 = [1,2,3]
const array2 = [4,[5,[6]]]
flatten([array1, array2]) // [1,2,3,4,5,6]

Swap

交换两项的值返回一个新数组。

const swap = (a, i, j) => (
  map(a, (x,y) => {
    if(y === i) return a[j]
    if(y === j) return a[i]
    return x
  })
)

示例:

const array = [1,2,3,4,5]
swap(array, 0, 4) // [5,2,3,4,1]

Map

MDN: 数组中元素执行传入函数生成一个新的数组。

const map = ([x, ...xs], fn) => {
  if (undef(x)) return []
  return [fn(x), ...map(xs, fn)]
}

可以简化为

const map = ([x, ...xs], fn) => def(x) ? [fn(x), ...map(xs, fn)] : []

示例:

const double = x => x * 2
map([1,2,3], double) // [2,4,6]

Filter

MDN: 数组中元素执行传入函数通过验证执行生成一个新的数组。

const filter = ([x, ...xs], fn) => {
  if (undef(x)) return []
  if (fn(x)) {
    return [x, ...filter(xs, fn)]
  } else {
    return [...filter(xs, fn)]
  }
}

可以简化为

const filter = ([x, ...xs], fn) => def(x)
    ? fn(x)
        ? [x, ...filter(xs, fn)] : [...filter(xs, fn)]
    : []

示例:

const even = x => x % 2 === 0
const odd = x = !even(x)
const array = [1,2,3,4,5]

filter(array, even) // [2,4]
filter(array, odd) // [1,3,5]

Reject

与筛选器相反,返回一个不通过筛选器函数的数组。

const reject = ([x, ...xs], fn) => {
  if (undef(x)) return []
  if (!fn(x)) {
    return [x, ...reject(xs, fn)]
  } else {
    return [...reject(xs, fn)]
  }
}

示例:

const even = x => x % 2 === 0
const array = [1,2,3,4,5]

reject(array, even) // [1,3,5]

Partition

将一个数组分成两个数组。一个是其项通过筛选函数的,另一个是其项失败的。

const partition = (xs, fn) => [filter(xs, fn), reject(xs, fn)]

示例:

const even = x => x % 2 === 0
const array = [0,1,2,3,4,5]

partition(array, even) // [[0,2,4], [1,3,5]]

Reduce

MDN: 对累加器和数组中的每个元素(从左到右)应用的一个函数,将其结果汇总为单个返回值。

const reduce = ([x, ...xs], fn, memo, i) => {
  if (undef(x)) return memo
  return reduce(xs, fn, fn(memo, x, i), i + 1)
}

Which can be simplified as:

const reduce = ([x, ...xs], fn, memo, i = 0) => def(x)
    ? reduce(xs, fn, fn(memo, x, i), i + 1) : memo

示例:

const sum = (memo, x) => memo + x
reduce([1,2,3], sum, 0) // 6

const flatten = (memo, x) => memo.concat(x)
reduce([4,5,6], flatten, [1,2,3]) // [1,2,3,4,5,6]

ReduceRight

与reduce相似,但是从右到左应用函数。

const reduceRight = (xs, fn, memo) => reduce(reverse(xs), fn, memo)

示例:

const flatten = (memo, x) => memo.concat(x)

reduceRight([[0,1], [2,3], [4,5]], flatten, []) // [4, 5, 2, 3, 0, 1]

Partial

通过填充任意数量的参数部分应用函数。

const partial = (fn, ...args) => (...newArgs) => fn(...args, ...newArgs)

示例:

const add = (x,y) => x + y
const add5to = partial(add, 5)

add5to(10) // 15

SpreadArg

将接受数组的函数转换为接受多个参数的函数。这在部分应用时非常有用。

const spreadArg = (fn) => (...args) => fn(args)

示例:

const add = ([x, ...xs]) => def(x) ? parseInt(x + add(xs)) : []
add([1,2,3,4,5]) // 15

const spreadAdd = spreadArg(add)
spreadAdd(1,2,3,4,5) // 15

如果你只想定义一个函数,你可以把它写成

const add = spreadArg(([x, ...xs]) => def(x) ? parseInt(x + add(...xs)) : [])
add(1,2,3,4,5) // 15

在上面的示例中,需要记住传递给递归函数的展开数组是使用展开的参数实现的。

ReverseArgs

反转函数参数的顺序。

const reverseArgs = (fn) => (...args) => fn(...reverse(args))

示例:

const divide = (x,y) => x / y
divide(100,10) // 10

const reverseDivide = reverseArgs(divide)
reverseDivide(100,10) // 0.1

对于部分应用参数来说,反向参数可能很有用。有时您希望使用部分列表末尾的参数,而不是列表开头的参数。翻转参数将允许我们做到这一点。

const percentToDec = partial(reverseDivide, 100)

percentToDec(25) // 0.25

Pluck

提取数组中的属性值。与 map 函数结合使用时非常有用。

const pluck = (key, object) => object[key]

示例:

const product = {price: 15}
pluck('price', product) // 15

const getPrices = partial(pluck, 'price')
const products = [
  {price: 10},
  {price: 5},
  {price: 1}
]
map(products, getPrices) // [10,5,1]

Flow

每个函数使用之前的函数的返回值。

const flow = (...args) => init => reduce(args, (memo, fn) => fn(memo), init)

示例:


const getPrice = partial(pluck, 'price')
const discount = x => x * 0.9
const tax = x => x + (x * 0.075)
const getFinalPrice = flow(getPrice, discount, tax)

// looks like: tax(discount(getPrice(x)))
// -> get price
// -> apply discount
// -> apply taxes to discounted price

const products = [
  {price: 10},
  {price: 5},
  {price: 1}
]

map(products, getFinalPrice) // [9.675, 4.8375, 0.9675]

Compose

与 flow 相同,但是参数使用时的顺序相反。Compose 与函数的编写方式更自然更匹配。使用与 flow 函数定义相同的数据:

const compose = (...args) => flow(...reverse(args))

示例:

const getFinalPrice = compose(tax, discount, getPrice)

// looks like: tax(discount(getPrice(x)))

map(products, getFinalPrice) // [9.675, 4.8375, 0.9675]

Min

返回数组中最小的数字。如果提供的数组为空,则返回 Infinity。

const min = ([x, ...xs], result = Infinity) => def(x)
    ? x < result
        ? min(xs, x)
        : result
    : result

示例:

const array = [0,1,2,3,4,5]

min(array) // 0

Max

返回数组中最大的数字。如果数组是空的,返回 Infinity。

const max = ([x, ...xs], result = -Infinity) => def(x)
    ? x > result
        ? max(xs, x)
        : max(xs, result)
    : result

示例:

const array = [0,1,2,3,4,5]

max(array) // 5

Factorial

返回一个数的阶乘。使用一个累加器来允许替换堆栈帧以允许返回更大的阶乘。

const factorial = (x, acum = 1) => x ? factorial(x - 1, x * acum) : acum

示例:

factorial(5) // 120

Fibonacci

返回给定位置的斐波那契数。

const fib = x => x > 2 ? fib(x - 1) + fib(x - 2) : 1

示例:

fib(15) // 610

Quicksort

对数组从小到大排序。通过重新排序数组来实现的,使其包含两个子数组,一个值较小,另一个值较大。使用递归应用于每个子数组,直到没有数组剩下,然后使用 flatten 函数返回排序后的数组。

const quicksort = (xs) => length(xs)
  ? flatten([
    quicksort(filter(tail(xs), x => x <= head(xs))),
    head(xs),
    quicksort(filter(tail(xs), x => x > head(xs)))
  ])
  : []

这也可以通过 partition 函数来实现,但需要赋值给一个新的变量。

const quicksort = (array) => {
  if (!length(array)) return []
  const [less, more] = partition(tail(array), x => x < head(array))
  return flatten([quicksort(less), head(array), quicksort(more)])
}

示例:

const array = [8,2,6,4,1]

quicksort(array) // [1,2,4,6,8]

所有函数都是 Reduce

上面的许多功能都可以转换为 reduce 函数,这在大多数情况下也会提高性能。这也彰显了reduce 函数的灵活性。

const reduce = ([x, ...xs], f, memo, i = 0) => def(x)
    ? reduce(xs, f, f(memo, x, i), i + 1) : memo

const reverse = xs => reduce(xs, (memo, x) => [x, ...memo], [])

const length = xs => reduce(xs, (memo, x) => memo + 1, 0)

const map = (xs, fn) => reduce(xs, (memo, x) => [...memo, fn(x)], [])

const filter = (xs, fn) => reduce(xs, (memo, x) => fn(x)
    ? [...memo, x] : [...memo], [])

const reject = (xs, fn) => reduce(xs, (memo, x) => fn(x)
    ? [...memo] : [...memo, x], [])

const first = (xs, n) => reduce(xs, (memo, x, i) => i < n
    ? [...memo, x] : [...memo], [])

const last = (xs, n) => reduce(xs, (memo, x, i) => i >= (length(xs) - n)
    ? [...memo, x] : [...memo], [])

const merge = spreadArg(xs => reduce(xs, (memo, x) => [...memo, ...x], []))

const flatten = xs => reduce(xs, (memo, x) => x
    ? isArray(x) ? [...memo, ...flatten(x)] : [...memo, x] : [], [])

const add = spreadArg(([x, ...xs]) => reduce(xs, (memo, y) => memo + y, x))

const divide = spreadArg(([x, ...xs]) => reduce(xs, (memo, y) => memo / y, x))

const multiply = spreadArg(([x, ...xs]) => reduce(xs, (memo, y) => memo * y, x))

示例:

reverse([1,2,3]) // [3,2,1]
length([1,2,3]) // 3
map([1,2,3], double) // [2,3,4]
filter([1,2,3,4], even) // [2,4]
reject([1,2,3,4], even) // [1,3]
first([1,2,3,4], 3) // [1,2,3]
last([1,2,3,4], 2) // [3,4]
merge([1,2,3],[4,5,6]) // [1,2,3,4,5,6]
flatten([1,[2,3,[4,[5,[[6]]]]]]) // [1,2,3,4,5,6]
add(1,2,3,4,5) // 15
multiply(2,5,10) // 100
divide(100,2,5) // 10

圆满完成

我希望这篇文章能帮助我们深入了解 JavaScript 和 ES6 提供的一些设计模式。很多问题可以用迭代或者循环来解决,当然也可以使用递归函数。我希望本文还能够向您展示 reduce 函数的灵活性。



blog comments powered by Disqus