JavaScript ES6函数式编程(一):闭包与高阶函数
函数式编程的历史
函数的第一原则是要小,第二原则则是要更小 —— ROBERT C. MARTIN
解释一下上面那句话,就是我们常说的一个函数只做一件事,比如:将字符串首字母和尾字母都改成大写,我们此时应该编写两个函数。为什么呢?为了更好的复用,这样做保证了函数更加的颗粒化。
早在 1950 年代,随着 Lisp 语言的创建,函数式编程( Functional Programming,简称 FP)就已经开始出现在大家视野。而直到近些年,函数式以其优雅,简单的特点开始重新风靡整个编程界,主流语言在设计的时候无一例外都会更多的参考函数式特性( Lambda 表达式,原生支持 map ,reduce ……),Java8 开始支持函数式编程。
而在前端领域,我们同样能看到很多函数式编程的影子:Lodash.js、Ramda.js库的广泛使用,ES6 中加入了箭头函数,Redux 引入 Elm 思路降低 Flux 的复杂性,React16.6 开始推出 React.memo(),使得 pure functional components 成为可能,16.8 开始主推 Hooks,建议使用 pure functions 进行组件编写……
这些无一例外的说明,函数式编程这种古老的编程范式并没有随着岁月而褪去其光彩,反而愈加生机勃勃。
什么是函数式编程
上面我们了解了函数式编程的历史,确定它是个很棒的东西。接下来,我们要去了解一下什么是函数式编程?
其实函数我们从小就学,什么一元函数(f(x) = 3x
),二元函数……根据学术上函数的定义,函数即是一种描述集合和集合之间的转换关系,输入通过函数都会返回有且只有一个输出值。
所以,函数实际上是一个关系,或者说是一种映射,而这种映射关系是可以组合的,一旦我们知道一个函数的输出类型可以匹配另一个函数的输入,那他们就可以进行组合。
在编程的世界里,我们需要处理其实也只有“数据”和“关系”,而“关系”就是函数,“数据”就是要传入的实参。我们所谓的编程工作也不过就是在找一种映射关系,比如:将字符串首字母转为大写。一旦关系找到了,问题就解决了,剩下的事情,就是让数据流过这种关系,然后转换成另一个数据返回给我们。
想象一个流水线车间的工作过程,把输入当做原料,把输出当做产品,数据可以不断的从一个函数的输出可以流入另一个函数输入,最后再输出结果,这不就是一套流水线嘛?
所以,现在你明确了函数式编程是什么了吧?它其实就是强调在编程过程中把更多的关注点放在如何去构建关系。通过构建一条高效的建流水线,一次解决所有问题。而不是把精力分散在不同的加工厂中来回奔波传递数据。
参考链接:阮一峰 - 函数式编程入门教程
函数式编程的特点
- 函数是一等公民
根据维基百科,编程语言中一等公民的概念是由英国计算机学家Christopher Strachey提出来的,时间则早在上个世纪60年代,那个时候还没有个人电脑,没有互联网,没有浏览器,也没有JavaScript。并且当时也没给出清晰的定义。
关于一等公民,我找到一个权威的定义,来自于一本书《Programming Language Pragmatics》,这本书是很多大学的程序语言设计的教材。
In general, a value in a programming language is said to have first-class status if it can be passed as a parameter, returned from a subroutine, or assigned into a variable.
也就是说,在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。
对于各种编程语言来说,函数就不一定是一等公民了,比如Java 8之前的版本。
对于JavaScript来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此JavaScript中函数是一等公民。
- 声明式编程 (Declarative Programming)
通过上面的例子可以看出来,函数式编程大多时候都是在声明我需要做什么,而非怎么去做。这种编程风格称为**声明式编程 **。
// 比如:我们要打印数组中的每个元素
// 1. 命令式编程
let arr = [1, 2, 3];
for (let i = 0, len = arr.length; i < len; i++) {
console.log(arr[i])
}
// 2. 声明式编程
let arr = [1, 2, 3];
arr.forEach(item => {
console.log(item)
})
/*
* 相对于命令式编程的 for 循环拿到每个元素,声明式编程不需要自己去找每个元素
* 因为 forEach 已经帮我们拿到了,就是 item,直接打印出来就行
*/
这样有个好处是代码的可读性特别高,因为声明式代码大多都是接近自然语言的,同时,它解放了大量的人力,因为它不关心具体的实现,因此它可以把优化能力交给具体的实现,这也方便我们进行分工协作。
- 惰性执行(Lazy Evaluation)
所谓惰性执行指的是函数只在需要的时候执行,即不产生无意义的中间变量。
- 无状态和数据不可变 (Statelessness and Immutable data)
这是函数式编程的核心概念:
数据不可变:它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
**无状态: **主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
- 没有副作用(side effect)
副作用,一般指完成分内的事情之后还带来了不好的影响。在函数中,最常见的副作用就是随意修改外部变量。由于js对象传递的是引用地址,这很容易带来bug。
例如: map 函数的本来功能是将输入的数组根据一个函数转换,生成一个新的数组。而在 JS 中,我们经常可以看到下面这种对 map 的 “错误” 用法,把 map 当作一个循环语句,然后去直接修改数组中的值。
const list = [...];
// 修改 list 中的 type 和 age
list.map(item => {
item.type = 1;
item.age++;
})
传递引用一时爽,代码重构火葬场
这样函数最主要的输出功能没有了,变成了直接修改了外部变量,这就是它的副作用。而没有副作用的写法应该是:
const list = [...];
// 修改 list 中的 type 和 age
const newList = list.map(item => ({...item, type: 1, age:item.age + 1}));
保证函数没有副作用,一来能保证数据的不可变性,二来能避免很多因为共享状态带来的问题。当你一个人维护代码时候可能还不明显,但随着项目的迭代,项目参与人数增加,大家对同一变量的依赖和引用越来越多,这种问题会越来越严重。最终可能连维护者自己都不清楚变量到底是在哪里被改变而产生 Bug。
- 纯函数 (pure functions)
函数式编程最关注的对象就是纯函数,纯函数的概念有两点:
不依赖外部状态(无状态): 函数的的运行结果不依赖全局变量,this 指针,IO 操作等。
没有副作用(数据不变): 不修改全局变量,不修改入参。
所以纯函数才是真正意义上的 “函数”, 它也遵循引用透明性——相同的输入,永远会得到相同的输出。
我们这么强调使用纯函数,纯函数的意义是什么?
便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。这十分符合测试驱动开发 TDD(Test-Driven Development ) 的思想,这样产生的代码往往健壮性更强。
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果,有很多库有所谓的 memoize 函数,下面以一个简化版的 memoize 为例,这个函数就能缓存函数的结果,对于像 fibonacci 这种计算,就可以起到很好的缓存效果。
function memoize(fn) {
const cache = {};
return function() {
const key = JSON.stringify(arguments);
var value = cache[key];
if(!value) {
value = [fn.apply(null, arguments)]; // 放在一个数组中,方便应对 undefined,null 等异常情况
cache[key] = value;
}
return value[0];
}
}
const fibonacci = memoize(n => n < 2 ? n: fibonacci(n - 1) + fibonacci(n - 2));
console.log(fibonacci(4)) // 执行后缓存了 fibonacci(2), fibonacci(3), fibonacci(4)
console.log(fibonacci(10)) // fibonacci(2), fibonacci(3), fibonacci(4) 的结果直接从缓存中取出,同时缓存其他的
闭包
定义:一个能够读取其他函数内部变量的函数,实质是变量的解析过程(由内而外)
闭包是ES中一个离不开的话题,而且也是是一个难懂又必须搞明白的概念!说起闭包,就不得不提与它密切相关的变量作用域和变量的生命周期。下面来看下:
变量作用域
变量作用域分为两类:全局作用域和局部作用域。
- 编写在script标签中的变量或者没用var关键字声明的变量,就代表全局变量,在页面的任意位置都可以访问到
- 在函数中声明变量带有var关键字的即是局部变量,局部变量只能在函数内才能访问到
function fn() {
var a = 1; // a为局部变量
console.log(a); // 1
}
fn();
console.log(a); // a is not defined 外部访问不到内部的变量
上面代码展示了在函数中声明的局部变量a在函数外部拿不到。可是我们就想要在函数外拿到它,怎么办?下面就要看发挥闭包的威力了。
函数可以创造函数作用域,在函数作用域中如果要查找一个变量的时候,如果在该函数内没有声明这个变量,就会向该函数的外层继续查找,一直查到全局变量为止。
所以变量的查找是由内而外的,这也形成了所谓的作用域链。
var a = 7;
function outer() {
var b = 8;
function inner() {
var c = 9;
alert(b);
alert(a);
}
inner();
alert(c); // c is not defined
}
outer(); // 调用函数
还是最开始的函数,利用作用域链,我们试着去拿到a,改造一下fn函数:
function fn() {
var a = 1; // a为局部变量
return function() {
console.log(a);
}
}
var fn2 = fn();
fn2(); // 1
理解了变量作用域,顺着这条作用域链,再来回顾一下闭包的定义:**闭包就是能够读取其他函数内部变量的函数,实质是变量的解析过程(由内而外) **
变量生命周期
理解了变量作用域,再来看看变量的生命周期,直白一点就是它能在程序中存活多久。
- 对于全局变量而言,它的生命周期机就是永久的,除非我们手动销毁它(这一点也是很有必要的,防止内存溢出)
- 对于在函数中通过var声明的变量而言,就没那么幸运了。当函数执行完毕后,它也就没什么利用价值了,随之被浏览器的垃圾处理机制当垃圾处理掉了
比如下面这段代码:
var forever = 'i am forever exist' // 全局变量,永生
function fn() {
var a = 123; // fn执行完毕后,变量a就将被销毁了
console.log(a);
}
fn();
函数执行完毕,内部的变量a就被无情的销毁了。那么我们有没有办法拯救这个变量呢?答案是肯定的,救星来了——闭包
闭包的创建
function outFn() {
var i = 1;
function inFn () {
return ++i
}
return inFn;
}
var fn = outFn(); // 此处创建了一个闭包
fn(); // 2
fn(); // 3
fn(); // 4
上面的代码创建了一个闭包,有两个特点:
- 函数inFn嵌套在函数outFn内部
- 函数outFn返回内部函数inFn
在执行完var fn = outFn();
后,变量 fn 实际上是指向了函数 inFn,再执行 fn( ) 后就会返回 i 的值(第一次为1)。这段代码其实就创建了一个闭包,这是因为函数 outFn 外的变量 fn 引用了函数 outFn 内的函数inFn。也就是说,当函数 outFn 的内部函数 inFn 被函数 outFn 外的一个变量 fn 引用的时候,就创建了一个闭包(函数内部的变量 i 被保存到内存中,不会被立即销毁)。
高阶函数
定义:高阶函数就是接受函数作为参数或者返回函数作为输出的函数。
下面分两种情况讲解,搞清这两种应用场景,这将有助于理解并运用高阶函数。
函数作为参数传入
函数作为参数传入最常见的就是回调函数。例如:在 ajax 异步请求的过程中,回调函数使用的非常频繁。因为异步执行不能确定请求返回的时间,将callback回调函数当成参数传入,待请求完成后执行 callback 函数。
$.ajax({
url: 'http://musicapi.leanapp.cn/search', // 以网易云音乐为例
data: {
keywords
},
success: function (res) {
callback && callback(res.result.songs);
}
})
函数作为返回值输出
函数作为返回值输出的应用场景那就太多了,这也体现了函数式编程的思想。其实从闭包的例子中我们就已经看到了关于高阶函数的相关内容了。
还记得在我们去判断数据类型的时候,我们都是通过Object.prototype.toString
来计算的,每个数据类型之间只是'[object XXX]'
不一样而已。
下面我们封装一个高阶函数,实现对不同类型变量的判断:
function isType (type) {
return function (obj) {
return Object.prototype.toString.call(obj) === `[object ${type}]
}
}
const isArray = isType('Array'); // 判断数组类型的函数
const isString = isType('String'); // 判断字符串类型的函数
console.log(isArray([1, 2]); // true
console.log(isString({}); // false
参考链接:
高阶函数,你怎么那么漂亮呢!
简明 JavaScript 函数式编程——入门篇
总结
最后总结一下这次的重点:纯函数、变量作用域、闭包、高阶函数。
- 纯函数的定义:给定的输入返回相同的输出的函数。
- 变量作用域是闭包的实质。根据变量作用域向上查找的特性,闭包可以缓存变量到内存中,函数执行完毕不会立即销毁。
- 高阶函数的核心是闭包,利用闭包缓存一些未来会用到的变量,可以实现柯里化、偏应用...
下一节介绍柯里化、偏应用、组合、管道...
JavaScript ES6函数式编程(一):闭包与高阶函数的更多相关文章
- Java函数式编程:二、高阶函数,闭包,函数组合以及柯里化
承接上文:Java函数式编程:一.函数式接口,lambda表达式和方法引用 这次来聊聊函数式编程中其他的几个比较重要的概念和技术,从而使得我们能更深刻的掌握Java中的函数式编程. 本篇博客主要聊聊以 ...
- Python 函数式编程 & Python中的高阶函数map reduce filter 和sorted
1. 函数式编程 1)概念 函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念.wiki 我们知道,对象是面向对象的第一型,那么函数式编程也是一样,函数是函数 ...
- python学习,day3:函数式编程,递归和高阶函数
# coding=utf-8 # Author: RyAn Bi def calc(n): #递归 print(n) if int(n/2) > 0: #设置条件,否则会循环999 次,报错, ...
- 《JavaScript ES6 函数式编程入门经典》--推荐指数⭐⭐⭐
这本书比较基础认真看完再自己写点demo一个双休日就差不多, 总体来说看完还是有收获的,会激起一些你对函数编程的兴趣 主要目录如下: 第1章 函数式编程简介 11.1 什么是函数式编程?为何它重要 1 ...
- JavaScript ES6函数式编程(二):柯里化、偏应用和组合、管道
上一篇介绍了闭包和高阶函数,这是函数式编程的基础核心.这一篇来看看高阶函数的实战场景. 首先强调两点: 注意闭包的生成位置,清楚作用域链,知道闭包生成后缓存了哪些变量 高阶函数思想:以变量作用域作为根 ...
- JavaScript之闭包与高阶函数(一)
JavaScript虽是一门面向对象的编程语言,但同时也有许多函数式编程的特性,如Lambda表达式,闭包,高阶函数等. 函数式编程是种编程范式,它将电脑运算视为函数的计算.函数编程语言最重要的基础是 ...
- Javascript 闭包与高阶函数 ( 二 )
在上一篇 Javascript 闭包与高阶函数 ( 一 )中介绍了两个闭包的作用. 两位大佬留言指点,下来我会再研究闭包的实现原理和Javascript 函数式编程 . 今天接到头条 HR 的邮件,真 ...
- Javascript 闭包与高阶函数 ( 一 )
上个月,淡丶无欲 让我写一期关于 闭包 的随笔,其实惭愧,我对闭包也是略知一二 ,不能给出一个很好的解释,担心自己讲不出个所以然来. 所以带着学习的目的来写一写,如有错误,忘不吝赐教 . 为什么要有闭 ...
- [Node.js] 闭包和高阶函数
原文地址:http://www.moye.me/2014/12/29/closure_higher-order-function/ 引子 最近发现一个问题:一部分写JS的人,其实对于函数式编程的概念并 ...
随机推荐
- 为什么Hashtable ConcurrentHashmap不支持key或者value为null
ConcurrentHashmap HashMap和Hashtable都是key-value存储结构,但他们有一个不同点是 ConcurrentHashmap.Hashtable不支持key或者val ...
- 在eclipse中引入mybatis和spring的约束文件
eclipse中引入mybatis约束文件步骤: 首先: config的key值 http://mybatis.org/dtd/mybatis-3-config.dtd mapper的key值 htt ...
- FreeSql (二十)多表查询 WhereCascade
WhereCascade 多表查询时非常方便,有了它可以很轻松的完成类型软删除,租户条件的功能. IFreeSql fsql = new FreeSql.FreeSqlBuilder() .UseCo ...
- 高性能最终一致性框架Ray之基本概念原理
一.Actor介绍 Actor是一种并发模型,是共享内存并发模型的替代方案. 共享内存模型的缺点: 共享内存模型使用各种各样的锁来解决状态竞争问题,性能低下且让编码变得复杂和容易出错. 共享内存受限于 ...
- Django-官网查询部分翻译(1.11版本文档)-QuerySet-字段查找-06
目录 Making queries 进行查询 创建一个对象(一条数据记录) 保存修改的表对象 保存外键字段或多对多字段(ForeignKey or ManyToManyField fields) Re ...
- mybatis无法给带有下划线属性赋值问题
https://blog.csdn.net/qq_33768099/article/details/69569561
- 修改tomcat 使用的JVM的内存
一,前言 在文章让tomcat使用指定JDK中,我让tomcat成功使用了我指定的JDK1.8,而不是环境变量中配置的JDK10.本篇文章我们就来探讨一下怎么设置tomcat使用的JVM的内存. 为什 ...
- Spring入门(十四):Spring MVC控制器的2种测试方法
作为一名研发人员,不管你愿不愿意对自己的代码进行测试,都得承认测试对于研发质量保证的重要性,这也就是为什么每个公司的技术部都需要质量控制部的原因,因为越早的发现代码的bug,成本越低,比如说,Dev环 ...
- SpringBoot系列——ElasticSearch
前言 本文记录安装配置ES环境,在SpringBoot项目中使用SpringData-ElasticSearch对ES进行增删改查通用操作 ElasticSearch官网:https://www.el ...
- 在vue项目中使用less
1.安装 less 和 less-loader. 命令: npm install less less-loader --save-dev 2.打开 build/webpack.ba ...