热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

JavaScript函数式编程(一)

说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚。所以本文希

说到函数式编程,想必各位或多或少都有所耳闻,然而对于函数式的内涵和本质可能又有些说不清楚。

所以本文希望针对工程师,从应用(而非学术)的角度将函数式编程相关思想和实践(以 Javascript 为例)分享给大家。

文章内容其实主要来自于在下阅读各类参考文献后的再整理,所以有什么错误也希望大家帮忙斧正~

slide 地址

一、什么是函数式编程?

Functional programming is a programming paradigm
1.treats computation as the evaluation of mathematical functions
2.avoids changing-state and mutable data
by wikipedia

从以上维基百科的定义来看有三个要点

Programming Paradigm
Mathematical Functions
Changing-state And Mutable Data

下面分别解析一下以上要点。

1.1.什么是编程范式?

Javascript 函数式编程(一)

from Programming paradigms

编程范式从概念上来讲指的是编程的基本风格和典范模式。

换句话说其实就是 程序员 对于如何使用编程来解决问题的 世界观和方法论

如果把一门编程语言比作兵器,它的语法、 工具 和技巧等是招法,那么它采用的编程范式也就是是内功心法。

一种范式可以在不同的语言中实现,一种语言也可以同时支持多种范式。例如 Javascript 就是一种多范式的语言。

1.2.什么是数学函数?

一般的,在一个变化过程中,假设有两个变量 x、y,如果对于任意一个 x 都有唯一确定的一个y和它对应,那么就称 x 是自变量,y 是 x 的函数。x 的取值范围叫做这个函数的定义域,相应 y 的取值范围叫做函数的值域。

以上定义,在初中数学咱们都应该学过...

换句话说,函数只是两种数值之间的关系:输入和输出。

尽管每个输入都只会有一个输出,但不同的输入却可以有相同的输出。下图展示了一个合法的从 x 到 y 的函数关系;

Javascript 函数式编程(一)

与之相反,下面这张图表展示的就不是一种函数关系,因为输入值 5 指向了多个输出:

Javascript 函数式编程(一)

1.2.1.什么是纯函数(Pure Functions)?

纯函数是这样一种函数,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

根据定义可以看出纯函数其实就是数学函数,即表示从输入的参数到输出结果的映射。

而没有副作用的纯函数显然都是引用透明的。

引用透明性(Referential Transparency)指的是,如果一段代码在不改变整个程序行为的前提下,可以替换成它的执行结果。

const double = x => x * 2
const addFive = x => x + 5
const num = double(addFive(10))

num === double(10 + 5)
    === double(15)
    === 15 * 2
    === 30

不过说了半天,副作用又是啥...?

1.2.2.什么是副作用(Side Effects)?

副作用是在计算的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用可能包含,但不限于一下行为:

  • 更改文件系统
  • 往数据库中插入记录
  • 发送一个 http 请求
  • 改变数据
  • 打印 log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态
  • ...

只要是跟函数外部环境发生的交互就都是副作用——这一点可能会让你怀疑无副作用编程的可行性。

函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

当然这并不是说,要禁止使用一切副作用,而是说,要让它们在可控的范围内发生。

在后面讲到函子(functor)和单子(monad)的时候我们会学习如何控制它们。

1.2.3.纯函数的好处都有啥~~(谁说对了就给他)~~?

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

by Erlang 作者:Joe Armstrong

所以使用纯函数将会有以下好处:

  • 可缓存性(Cacheable)
  • 可移植性/自文档化(Portable / Self-Documenting)
  • 可测试性(Testable)
  • 合理性(Reasonable)
  • 并行代码(Parallel Code)

1.3.为什么要避免改变状态和可变数据?

Shared mutable state is the root of all evil

共享可变状态是万恶之源

by Pete Hunt

Javascript 函数式编程(一)
const obj = { val: 1 }
someFn(obj)
console.log(obj) // ???
Javascript 函数式编程(一)

from Building Scalable, Highly Concurrent & Fault Tolerant Systems - Lessons Learned

1.4.原教旨函数式 VS 温和派函数式?

说到函数式编程语言,大家的第一反应可能是 Haskell、OCaml、Lisp、Erlang、Scala、F#...

因为它们可能有以下特性:

  • 函数是“一等公民”(first class)
  • 不可变数据
  • 使用递归而不是循环
  • 柯里化
  • 惰性求值
  • 代数数据类型
  • 模式匹配
  • ...
Javascript 函数式编程(一)

而说到 Javascript,很多人可能第一反应认为这是一门面向对象的语言。

但是想想前面说的:函数式编程只是一种编程范式,而编程范式就像“内功心法”,所以与以上这些语言特性不完全相关,反而与你自己的编程思维(即世界观和方法论)更加相关。

在函数式方面,由于 Javascript 支持高阶函数、匿名函数、函数是一等公民、闭包、解构(模式匹配)等特性,所以它也能支持函数式编程范式。(虽然不是那么的原教旨函数式,但还基本够用~尤其是 ES6 新增的箭头函数等特性~还有各种类库 )

事实上 Javascript 是一门基于原型(prototype-based)的 多范式 语言。

1.5.1.不可变数据结构

Javascript 一共有 6 种原始类型(包括 ES6 新添加的 Symbol 类型),它们分别是 Boolean,Null,Undefined,Number,String 和 Symbol。 除了这些原始类型,其他的类型都是 Object,而 Object 都是可变的。

Javascript 函数式编程(一)

1.5.2.惰性求值

惰性(lazy)指求值的过程并不会立刻发生。

比如一些数学题,我们可能一开始并不需要把所有表达式都求值,这样可以在计算的过程中将一些表达式消掉。

惰性求值是相对于**及早求值(eager evaluation)**的。

比如大部分语言中,参数中的表达式都会被先求值,这也称为 应用序 语言。

比如看下面这样一个 Javascript 的函数:

wholeNameOf(getFirstName(), getLastName())

getFirstNamegetLastName 会依次执行,返回值作为 wholeNameOf 函数的参数, wholeNameOf 函数最后才被调用。

另外,对于数组操作时,大部分语言也同样采用的是应用序。

[1, 2, 3, 4].map(x => x + 1)

所以,这个表达式立刻会返回结果 [2, 3, 4, 5] 。

当然这并不是说 Javascript 语言使用应用序有问题,但是没有提供惰性序列的支持就是 Javascript 的不对了。如果 map 一个大数组后我们发现其实只需要前 10 个元素时,去计算所有元素就显得是多余的了。

1.5.3.函数组合

面向对象通常被比喻为名词,而函数式编程是动词。面向对象抽象的是对象,对于对象的的描述自然是名词。

面向对象把所有操作和数据都封装在对象内,通过接受消息做相应的操作。比如,对象 Kitty,它们可以接受“打招呼”的消息,然后做相应的动作。

而函数式的抽象方式刚好相反,是把动作抽象出来,比如“打招呼”就是一个函数,而函数参数就是作为数据传入的 Kitty(即 Kitty 进入函数“打招呼”,出来的应该是 Hello Kitty)。

面向对象可以通过继承和组合在对象之间分享一些行为或者说属性,函数式的思路就是通过 组合 已有的函数形成一个新的函数。

然而 Javascript 语言虽然支持高阶函数,但是并没有一个原生的利于组合函数产生新函数的方式。而这些强大的函数组合方式却往往被类似 Underscore,Lodash 等工具库的光芒掩盖掉(后面会说到这些库的问题)。

1.5.4.尾递归优化

Javascript 函数式编程(一)

函数式编程语言中因为不可变数据结构的原因,没办法实现循环。所以都是通过递归来实现循环。

然而递归使用不当很容易栈溢出(Stack Overflow),所以一般采用尾递归的方式来优化。

虽然 ES6 规范中规定了尾递归优化规范,然而提供实现的解释器还非常的少,详情可以查阅这个链接

5.代数类型系统

Javascript 作为一种弱类型的语言,没有静态类型系统。不过使用一些 TypeScript 等预编译的语言可以作为补充~

二、声明式 VS 命令式

Declarative VS Imperative,这两者的区别简单来说其实就是 What VS How。

2.1.“意识形态”上的区别~

声明式:

  • 程序抽象了控制流过程,代码描述的是 —— 数据流:即做什么。
  • 更多依赖表达式。

表达式是指一小段代码,它用来计算某个值。表达式通常是某些函数调用的复合、一些值和操作符,用来计算出结果值。

命令式:

  • 代码描述用来达成期望结果的特定步骤 —— 控制流:即如何做。
  • 频繁使用语句。

语句是指一小段代码,它用来完成某个行为。通用的语句例子包括 for、if、switch、throw,等等……

2.2.举一些栗子:chestnut:...

例1:希望得到一个数组每个数据平方后的和

// 命令式
function mysteryFn (nums) {
  let squares = []
  let sum = 0                           // 1. 创建中间变量

  for (let i = 0; i  nums
  .map(x => x * x)                      // a. 平方
  .reduce((acc, cur) => acc + cur, 0)   // b. 累加

例2:希望得到一个数组所有偶数值的一半的平均值

// 命令式
function mysteryFn(nums) {
  let sum = 0
  let tally = 0                         // 1. 创建中间变量

  for (let i = 0; i  nums
  .filter(x => x % 2 === 0)             // a. 过滤非偶数
  .map(x => x / 2)                      // b. 折半
  .reduce((acc, cur, idx, { length: len }) => (
    idx  

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们


推荐阅读
  • ①页面初始化----------收到客户端的请求,产生相应页面的Page对象,通过Page_Init事件进行page对象及其控件的初始化.②加载视图状态-------ViewSta ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 浅解XXE与Portswigger Web Sec
    XXE与PortswiggerWebSec​相关链接:​博客园​安全脉搏​FreeBuf​XML的全称为XML外部实体注入,在学习的过程中发现有回显的XXE并不多,而 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文是一位90后程序员分享的职业发展经验,从年薪3w到30w的薪资增长过程。文章回顾了自己的青春时光,包括与朋友一起玩DOTA的回忆,并附上了一段纪念DOTA青春的视频链接。作者还提到了一些与程序员相关的名词和团队,如Pis、蛛丝马迹、B神、LGD、EHOME等。通过分享自己的经验,作者希望能够给其他程序员提供一些职业发展的思路和启示。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了在插入,更新或删除操作期间,在实体上找不到属性?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 原文链接:http:littleq.logdown.composts20131011kerl-to-manage-erlang-versions-on-your-system最近又回到 ... [详细]
author-avatar
朗月清风的海角
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有