Js函数式编程,给你的代码增加一点点函数式编程的特性

更新日期: 2017-11-07阅读: 3.4k标签: 函数作者: Peeke Kuepers

-- 给你的代码增加一点点函数式编程的特性

最近我对函数式编程非常感兴趣。这个概念让我着迷:应用数学来增强抽象性和强制纯粹性,以避免副作用,并实现代码的良好可复用性。同时,函数式编程非常复杂。

函数式编程有一个非常陡峭的学习曲线,因为它来源于数学中的范畴论。接触不久之后,就将遇到诸如组合(composition)、恒等(identity),函子(functor)、单子(monad),以及逆变(contravariant)等术语。我根本不太了解这些概念,可能这也是我从来没有在实践中运用函数式编程的原因。

我开始思考:在常规的命令式编程和完全的函数式编程之间是否可能会有一些中间形式?既允许在代码库引入函数式编程的一些很好的特性,同时暂时保留已有的旧代码。

对我而言,函数式编程最大的作用就是强制你编写声明性代码:代码描述你做什么,而不是在描述如何做。这样就可以轻松了解特定代码块的功能,而无需了解其真正的运行原理。事实证明,编写声明式代码是函数式编程中最简单的部分之一。

循环

...一个循环就是一个命令式控制结构,难以重用,并且难以插入到其他操作中。此外,它还得不断变化代码来响应新的迭代需求。
-- Luis Atencio

所以,让我们先看一下循环,循环是命令式编程的一个很好的例子。循环涉及很多语法,都是描述它们的行为是如何工作,而不是它们在做什么。例如,看看这段代码:

function helloworld(arr) {
    for (let i = 1; i < arr.length; i++) {
        arr[i] *= 2
        if (arr[i] % 2 === 0) {
            doSomething(arr[i])
        }
    }
}

这段代码在做什么呢?它将数组内除第一个数字 (let i = 1)的其他所有数字乘以 2,如果是偶数的话(if (arr % 2 === 0)),就进行某些操作。在此过程中,原始数组的值会被改变。但这通常是不必要的,因为数组可能还会在代码库中的其他地方用到,所以刚才所做的改变可能会导致意外的结果。

但最主要的原因是,这段代码看起来很难一目了然。它是命令式的,for 循环告诉我们如何遍历数组,在里面,使用一个 if 语句有条件地调用一个函数。

我们可以通过使用数组方法以声明式的方式重写这段代码。数组方法直接表达所做的事,比较常见的方法包括:forEach,map,filter,reduce 和 slice。

结果就像下面这样:

function helloworld(arr) {
    const evenNumbers = n => n % 2 === 0

    arr
        .slice(1)
        .map(v => v * 2)
        .filter(evenNumbers)
        .forEach(v => doSomething(v))    
}

在这个例子中,我们使用一种很好的,扁平的链式结构去描述我们在做什么,明确表明意图。此外,我们避免了改变原始数组,从而避免不必要的副作用,因为大多数数组方法会返回一个新数组。当箭头函数开始变得越来越复杂时,可以地将其提取到一个特定的函数中,比如 evenNumbers, 从而尽量保持结构简单易读。

在上面的例子,链式调用并没有返回值,而是以 forEach 结束。然而,我们可以轻松地剥离最后一部分,并返回结果,以便我们可以在其他地方处理它。如果还需要返回除数组以外的任何东西,可以使用 reduce 函数。

对于接下来的一个例子,假设我们有一组 JSON 数据,其中包含在一个虚构歌唱比赛中不同国家获得的积分:

[
    {
        "country": "NL",
        "points": 12
    },
    {
        "country": "BE",
        "points": 3
    },
    {
        "country": "NL",
        "points": 0
    },
    ...
]

我们想计算荷兰(NL)获得的总积分,根据印象中其强大的音乐能力,我们可以认为这是一个非常高的分数,但我们想要更精确地确认这一点。

使用循环可能会是这样:

function countVotes(votes) {
    let score = 0;

    for (let i = 0; i < votes.length; i++) {
        if (votes[i].country === 'NL') {
            score += votes[i].points;
        }
    }

    return score;
}

使用数组方法重构,我们得到一个更干净的代码片段:

function countVotes(votes) {
    const sum = (a, b) => a + b;

    return votes
        .filter(vote => vote.country === 'NL')
        .map(vote => vote.points)
        .reduce(sum);
}

有时候 reduce 可能有点难以阅读,将 reduce 函数提取出来会在理解上有帮助。在上面的代码片段中,我们定义了一个 sum 函数来描述函数的作用,因此方法链仍然保持很好的可读性。

if else 语句

接下来,我们来聊聊大家都很喜欢的 if else 语句,if else 语句也是命令式代码里一个很好的例子。为了使我们的代码更具声明式,我们将使用三元表达式。

一个三元表达式是 if else 语句的替代语法。以下两个代码块具有相同的效果:

// Block 1
if (condition) {
    doThis();
} else {
    doThat();
}

// Block 2
const value = condition ? doThis() : doThat();

当在定义(或返回)一个常量时,三元表达式非常有用。使用 if else 语句会将该变量的使用范围限制在语句内,通过使用三元语句,我们可以避免这个问题:

if (condition) {
    const a = 'foo';
} else {
    const a = 'bar';
}

const b = condition ? 'foo' : 'bar';

console.log(a); // Uncaught ReferenceError: a is not defined
console.log(b); // 'bar'

现在,我们来看看如何应用这一点来重构一些更重要的代码:

const box = element.getBoundingClientRect();

if (box.top - document.body.scrollTop > 0 && box.bottom - document.body.scrollTop < window.innerHeight) {
    reveal();
} else {
    hide();
}

那么,上面的代码发生了什么呢?if 语句检查元素当前是否在页面的可见部分内,这个信息在代码的任何地方都没有表达出来。基于此布尔值,再调用 reveal() 或者 hide() 函数。

将这个 if 语句转换成三元表达式迫使我们将条件移动到它自己的变量中。这样我们可以将三元表达式组合在一行上,现在通过变量的名称来传达布尔值表示的内容,这样还不错。

const box = element.getBoundingClientRect();
const isInViewport = 
    box.top - document.body.scrollTop > 0 && 
    box.bottom - document.body.scrollTop < window.innerHeight;

isInViewport ? reveal() : hide();

通过这个例子,重构带来的好处可能看起来不大。接下来会有一个相比更复杂的例子:

elements
    .forEach(element => {
        const box = element.getBoundingClientRect();

        if (box.top - document.body.scrollTop > 0 && box.bottom - document.body.scrollTop < window.innerHeight) {
            reveal();
        } else {
            hide();
        }

    });

这很不好,打破了我们优雅的扁平的调用链,从而使代码更难读。我们再次使用三元操作符,而在使用它的时候,使用 isInViewport 检查,并跟它自己的动态函数分开。

const isInViewport = element => {
    const box = element.getBoundingClientRect();
    const topInViewport = box.top - document.body.scrollTop > 0;
    const bottomInViewport = box.bottom - document.body.scrollTop < window.innerHeight;
    return topInViewport && bottomInViewport;
};

elements
    .forEach(elem => isInViewport(elem) ? reveal() : hide());

此外,现在我们将 isInViewport 移动到一个独立函数,可以很容易地把它放在它自己的 helper 类/对象之内:

import { isInViewport } from 'helpers';

elements
    .forEach(elem => isInViewport(elem) ? reveal() : hide());

虽然上面的例子依赖于所处理的是数组,但是在不明确是在数组的情况下,也可以采用这种编码风格。

例如,看看下面的函数,它通过三条规则来验证密码的有效性。

import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value => {
  if (value.length < 6) return false
  if (!requiredChars.test(value)) return false

  const forbidden = await getJson('/forbidden-passwords')
  if (forbidden.includes(value)) return false

  return value
}

validatePassword(someValue).then(persist)

如果我们使用数组包装初始值,就可以使用在上面的例子中里面所用到的所有数组方法。此外,我们已经将验证函数打包成 validationRules 使其可重用。

import { minLength, matchesRegex, notBlacklisted } from 'validationRules'
import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value => {
  const result = Array.from(value)
    .filter(minLength(6))
    .filter(matchesRegex(requiredChars))
    .filter(await notBlacklisted('/forbidden-passwords'))
    .shift()

  if (result) return result
  throw new Error('something went wrong...')
}

validatePassword(someValue).then(persist)

目前在 JavaScript 中有一个 管道操作符 的提案。使用这个操作符,就不用再把原始值换成数组了。可以直接在前面的值调用管道操作符之后的函数,有点像 Array 的 map 功能。修改之后的代码大概就像这样:

import { minLength, matchesRegex, notBlacklisted } from 'validationRules'
import { passwordRegex as requiredChars } from 'regexes'
import { getJson } from 'helpers'

const validatePassword = async value =>
  value
    |> minLength(6)
    |> matchesRegex(requiredChars)
    |> await notBlacklisted('/forbidden-passwords')

try { someValue |> await validatePassword |> persist }
catch(e) {
  // handle specific error, thrown in validation rule
}

但需要注意的是,这仍然是一个非常早期的提案,不过可以稍微期待一下。

事件

最后,我们来看看事件处理。一直以来,事件处理很难以扁平化的方式编写代码。可以 Promise 化来保持一种链式的,扁平化的编程风格,但 Promise 只能 resolve 一次,而事件绝对会多次触发。

在下面的示例中,我们创建一个类,它对用户的每个输入值进行检索,结果是一个自动补全的数组。首先检查字符串是否长于给定的阈值长度。如果满足条件,将从服务器检索自动补全的结果,并将其渲染成一系列标签。

注意代码的不“纯”,频繁地使用 this 关键字。几乎每个函数都在访问 this 这个关键字:

译注:作者在这里使用 "this keyword",有一种双关的意味
import { apiCall } from 'helpers'

class AutoComplete {

  constructor (options) {

    this._endpoint = options.endpoint
    this._threshold = options.threshold
    this._inputElement = options.inputElement
    this._containerElement = options.list

    this._inputElement.addEventListener('input', () =>
      this._onInput())

  }

  _onInput () {

    const value = this._inputElement.value

    if (value > this._options.threshold) {
      this._updateList(value)
    }

  }

  _updateList (value) {

    apiCall(this._endpoint, { value })
      .then(items => this._render(items))
      .then(html => this._containerElement = html)

  }

  _render (items) {

    let html = ''

    items.forEach(item => {
      html += `<a href="${ item.href }">${ item.label }</a>`
    })

    return html

  }

}

通过使用 Observable,我们将用一种更好的方式对这段代码进行重写。可以简单将 Observable 理解成一个能够多次 resolve 的 Promise。

Observable 类型可用于基于推送模型的数据源,如 dom 事件,定时器和套接字

Observable 提案目前处于 Stage-1。在下面 listen 函数的实现是从 GitHub 上的提案中直接复制的,主要是将事件监听器转换成 Observable。可以看到,我们可以将整个 AutoComplete 类重写为单个方法的函数链。

import { apiCall, listen } from 'helpers';
import { renderItems } from 'templates'; 

function AutoComplete ({ endpoint, threshold, input, container }) {

  listen(input, 'input')
    .map(e => e.target.value)
    .filter(value => value.length >= threshold)
    .forEach(value => apiCall(endpoint, { value }))
    .then(items => renderItems(items))
    .then(html => container.innerHTML = html)

}

由于大多数 Observable 库的实现过于庞大,我很期待 ES 原生的实现。map,filter和 forEach方法还不是规范的一部分,但是在 zen-observable 已经在扩展 API 实现,而 zen-observable 本身是 ES Observables 的一种实现 。

--

我希望你会对这些“扁平化”模式感兴趣。就个人而言,我很喜欢以这种方式重写我的程序。你接触到的每一段代码都可以更易读。使用这种技术获得的经验越多,就越来越能认识到这一点。记住这个简单的法则:

The flatter the better!


原文:Writing flat & declarative code
作者:Peeke Kuepers

链接: https://fly63.com/article/detial/63

JavaScript 函数式编程

我理解的 JavaScript 函数式编程,都认为属于函数式编程的范畴,只要他们是以函数作为主要载体的。

让我们来创建一个JavaScript Wait函数

Async/await以及它底层promises的应用正在猛烈地冲击着JS的世界。在大多数客户端和JS服务端平台的支持下,回调编程已经成为过去的事情。当然,基于回调的编程很丑陋的。

JavaScript函数创建的细节

如果你曾经了解或编写过JavaScript,你可能已经注意到定义函数的方法有两种。即便是对编程语言有更多经验的人也很难理解这些差异。在这篇博客的第一部分,我们将深入探讨函数声明和函数表达式之间的差异。

编写小而美函数的艺术

随着软件应用的复杂度不断上升,为了确保应用稳定且易拓展,代码质量就变的越来越重要。不幸的是,包括我在内的几乎每个开发者在职业生涯中都会面对质量很差的代码。这些代码通常有以下特征:

javascript回调函数的理解和使用方法(callback)

在js开发中,程序代码是从上而下一条线执行的,但有时候我们需要等待一个操作结束后,再进行下一步操作,这个时候就需要用到回调函数。 在js中,函数也是对象,确切地说:函数是用Function()构造函数创建的Function对象。

js调用函数的几种方法_ES5/ES6的函数调用方式

这篇文章主要介绍ES5中函数的4种调用,在ES5中函数内容的this指向和调用方法有关。以及ES6中函数的调用,使用箭头函数,其中箭头函数的this是和定义时有关和调用无关。

JavaScript中函数的三种定义方法

函数的三种定义方法分别是:函数定义语句、函数直接量表达式和Function()构造函数的方法,下面依次介绍这几种方法具体怎么实现,在实际编程中,Function()构造函数很少用到,前两中定义方法使用比较普遍。

js在excel的编写_excel支持使用JavaScript自定义函数编写

微软 称excel就实现面向开发者的功能,也就是说我们不仅可以全新定义的公式,还可以重新定义excel的内置函数,现在Excel自定义函数增加了使用 JavaScript 编写的支持,下面就简单介绍下如何使用js来编写excel自定义函数。

js中的立即执行函数的写法,立即执行函数作用是什么?

这篇文章主要讲解:js立即执行函数是什么?js使用立即执行函数有什么作用呢?js立即执行函数的写法有哪些?

js函数内部两个特殊的对象之arguments和this

arguments是一个类数组对象。包含着传入函数中的所有参数。但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。this引用的是函数执行的环境对象(当在网页的全局作用域中调用函数时,this对象引用的就是window)

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!