《Functional Programming in JavaScript》Note - Part1

《Functional Programming in JavaScript》笔记

这本书中的示例代码可以这里找到:https://github.com/luijar/functional-programming-js

Part 1 —— 入门

Why Functional?

1)什么是函数式编程?

以往我们也会写大量的function来解决问题,但是我们的function中封装的基本都是解决问题的过程,在函数式编程中,function不仅要来计算出一个结果,更要用它来抽象出控制流和对数据的操作,进而达到避免副作用、减少程序中的状态变化。函数式编程不是一种特殊的工具,而是一种解决问题的思路。

先来看个例子,在HTML中动态显示Hello World。最简单的做法:

1
document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>';

这种写法虽然简单,但是所有的东西都是硬编码的,但凡要改变元素、标签或展示的内容,都需要重新更改上面的表达式。此时你可能想说那就把所有的可能变化的部分作为参数,然后封装一个function,如下:

1
2
3
function printMessage(elementId, format, message) {
document.querySelector(`#${elementId}`).innerHTML = `<${format}>${message}</${format}>`
}

这样的写法的确有所改善,但是可复用的粒度还不够,比如我不往HTML中展示东西了,而是往文件或者控制台中展示,上面的function又不适用了。我们将上面的功能进一步拆分成每个只干一件事的function,然后再按需组合起来,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function addToDom(elementId) {
return function(content) {
document.querySelector(`#${elementId}`).innerHTML = content
}
}

function h1(message) {
return '<h1>' + message + '</h1>'
}

function echo(message) {
return message
}

var run = function(f, g, h) {
return function(x) {
return f(g(h(x)))
}
}

var printMessage = run(addToDom('msg'), h1, echo)
printMessage('Hello World')

此时,如果换成往控制台重复打印,并将输出内容换成h2标签,则可以很快的实现,如下:

1
2
var printMessage = run(console.log, repeat(3), h1, echo)
printMessage('Get Functional')

将一个酒店改造成KTV,可行,代价却很大,但是用一堆建筑材料来装修KTV,就容易很多了。要全面理解函数式编程还需要了解以下几个概念:

  • 声明式编程
  • 纯函数
  • 引用透明性
  • 不可变性

函数式编程是声明式的

我们最熟悉的面向对象编程(OOP)是命令式的,命令式的做法通常会定义一个从上到下改变程序状态的语句序列,以此来得到计算结果。比较典型的特征就是使用if、for、switch-case等来表述处理过程,最终得到一个结果。命令式频繁使用语句,代码描述的是控制流:即怎么做。

函数式编程(FP)是声明式的,也就是组合一系列表达式、操作符等来计算得到结果。声明式更多依赖表达式,代码描述的是数据流:即做什么。

看个例子对比一下,求一组数字的平方,命令式的做法如下:

1
2
3
4
5
6
var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for(let i = 0; i < array.length; i++) {
array[i] = Math.pow(array[i], 2) //循环计算每个数字的平方
}

//命令式编程详细地告诉计算机要怎样得到最终的结果

而声明式的做法如下:

1
2
3
4
5
6
7
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(num) {
return Math.pow(num, 2) //map接收一个function来计算每个数字的平方
})

//此时完全不用关心循环中的变量和过程中的取值了
//ES6中引入了lambda表达式,上面可以进一步简化为
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => Math.pow(num, 2))

这里说一下.map方法,.map是无法被break的,如果需要中断循环可以使用Array.prototype.some.map主要的用途不在于循环,而是当你想要得到一个新数组时才使用。

纯函数和副作用

先看一个不是纯函数,会产生副作用的例子:

1
2
3
4
5
6
var counter = 0            //全局变量
function increment() { //产生副作用:全局变量被改变了
return ++counter
}

//increment()的返回值变得不可预测,counter还有可能在别处被改变。

那什么是纯函数?

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

纯函数具有如下特性:

  • 输出结果只依赖于输入,不依赖于任何在它被调用或执行期间可能会发生变化的隐藏的或外部的状态。
  • 不会造成它范围之外的改变,例如修改一个全局对象或一个传入的参数。

什么又是副作用呢?

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

副作用会在很多场景中产生,其中包括:

  • 改变全局的变量、属性或者数据结构
  • 改变函数参数的初始值
  • 处理用户输入
  • 抛出异常,处分异常在同一个函数中被捕获到
  • 像屏幕或日志打印
  • 查询HTML documents,浏览器cookies,或database

再来看一个稍微复杂一点的例子,通过社交帐号从数据库中查询对应的学生信息,然后展示在页面上。用带有副作用的命令式编程的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function showStudent(ssn) {
var student = db.get(ssn) //暂时认为读db是一个同步的操作
if(student !== null) {
document.querySelector(`#${elementId}`).innerHTML = //读取函数之外的变量elementId
`${student.ssn},
${student.firstname},
${student.lastname}`

}
else {
throw new Error('Student not found!') //无效时抛出异常
}
}

showStudent('444-44-4444') //指定SSN为444-44-4444运行程序,并将得到的学生信息显示在页面中

这里存在的副作用有:

  • 使用函数外部的db来获取数据,因为在函数参数中并没有定义它。而该引用随时可能变成null或者被改变,导致得到一个不同的结果。
  • 全局变量elementId也有可能随时被改变。
  • HTML元素直接被改变了,而它本身是可变的、共享的、全局的资源。
  • 在学生未被查询到时会抛出异常,这会导致整个程序栈错误并突然结束。

对外部的依赖使得上面代码的可复用性、扩展性、可测试性都变得很难。现在用函数式的写法来分解上面的代码:

  • 将这个很长的函数拆分成多个短的函数,每一个函数只干一件事。
  • 明确指定函数需要的参数,以此来减少副作用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var find = curry(function(db, id) {
var obj = db.get(id)
if (obj === null) {
throw new Error('Object not found!')
}
})

//抛出异常的函数式处理会在Part2中讲到,需要先了解单子

var csv = (student) => {
return `${student.ssn}, ${student.firstname}, ${student.lastname}`
}

var append = curry(function(elementId, info) {
//将可变的部分单独抽离出来
document.querySelector(elementId).innerHTML = info
})

//curry的作用是把函数的参数减少到1个,Part2中会具体介绍

var showStudent = run(
append('#student-info'),
csv,
find(db))

showStudent('444-44-4444')

引用透明性和可替代性

如果一个函数对于同一输入始终产生相同的结果,则称其为引用透明性。

引用透明性使得我们能够推断出程序的运行结果,程序中的表达式可以替换成它的执行结果。将上面counter的例子改成纯函数,与之前的写法做一对比:

1
2
3
4
5
6
7
8
9
10
//命令式版本
increment()
increment()
print(counter) //-> ? 打印的结果依赖于counter的初始值,因此变得不可预测

//函数式版本
var increment = counter => counter + 1

var plus2 = run(increment, increment)
print(run(0)) //-> 2 打印的结果始终都是初始值加2

再来看个例子,计算学生的平均成绩:

1
2
3
4
5
6
7
8
9
10
11
var sum = (total, current) => total + current
var total = arr => arr.reduce(sum)
var size = arr => arr.length
var divide = (a, b) => a / b
var average = arr => divide(total(arr), size(arr))

var input = [80, 90, 100]
average(input) //-> 90

//因为divide具有引用透明性,所以完全可以将上面的表达式改写如下:
var average = divide(270, 3) //-> 90

保持数据不可变

不可变的数据是指一旦它被创建就不能再被改变了。在JavaScript中,所有的基本类型都是不可变的,如StringNumber。但是其他的对象,像数组,就是可变的,即使它作为参数被传入也有可能被改变,因而产生副作用。比如对数组进行倒序排列:

1
2
3
4
5
6
7
8
9
var sortDesc = function(arr) {
return arr.sort(function(a, b) {
return b - a
})
}

var arr = [1,2,3,4,5,6,7,8,9]
sortDesc(arr) //-> [9,8,7,6,5,4,3,2,1]
//arr的初始值被改变了

这种情况要怎样来克服呢?

了解了以上这些函数式编程的基本原则,现在我们可以用一句来概括一下:

函数式编程通过避免外部可观察的副作用来对纯函数进行声明式运算以创建不可变的程序。

2)函数式编程带来的好处

促使我们将复杂任务分解成简单的功能

函数式编程做的事其实就是分解和组合,就像前面在处理查询和展示学生信息的例子。先把任务分解成多个没有副作用的function,然后再通过一定手段将这些function组合起来。run其实是compose的别名,做的事就是让一个function的返回值成为下一个function的参数。

也可以用数学表达式的方式来表示:f • g = f(g(x))

用链式处理数据

要把分解后的function组合起来不止上面compose一种方式,还可以通过一连串的对上一个函数返回值的函数的调用,来得到最终的结果。这样使得每一步做的事情清晰可见,通过还隐藏的具体的处理细节。举个例子,假设我们要计算出注册了不止一个班级的学生的平均成绩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//采用命令式编程的方式实现
var totalGrades = 0
var totalStudentsFound = 0
for (let i = 0; i < enrollment.length; i++) {
let student = enrollment[i]
if (student !== null) {
if (student.enrolled > 1) {
totalGrades += student.grade
totalStudentsFound++
}
}
}
var average = totalGrades / totalStudentsFound //-> 90

//采用函数链的方式实现
_.chain(enrollment)
.filter(student => student.enrolled > 1)
.pluck('grade')
.average()
.value() //-> 90

上面的链式调用是用Lodash.js实现的。链式调用还能够做到惰性计算(Lazy Evaluation),按需调用,减少循环的次数和中间爱数组的产生,以此提高性能。关于惰性计算可以看一下这篇文章的讲解:https://segmentfault.com/a/1190000006998998

应对异步程序的复杂性

对于异步事件结果的处理我们通常用的是回调(callback),使用callback的问题在于它打破了代码的线性流程,一旦异步事件嵌套的层级多了,代码就变得难易阅读。近几年大热的React系列很好的解决了该问题。

这些React框架采用的响应式编程,其实就是基于函数式编程的。这些框架让事件(包括鼠标点击、输入框改变、光标改变、HTTP请求、数据库查询、文件写入等等)具有可观察性,事件的处理者就可以订阅事件产生的数据流,然后组合和链接一系列操作(如mapreduce…)对数据进行处理。这本书中用到的框架是RxJS

来看一个校验文本框输入的社交帐号是否有效的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//命令式编程的做法
var valid = false;
var elem = document.querySelector('#student-ssn');
elem.onkeyup = function (event) {
var val = elem.value;
if(val !== null && val.length !== 0) {
val = val.replace(/^\s*|\s*$|\-/g, '');
if(val.length === 9) {
console.log(`Valid SSN: ${val}!`);
valid = true;
}
}
else {
console.log(`Invalid SSN: ${val}!`);
}
};
//函数式编程的做法,使用RxJS框架处理
Rx.Observable.fromEvent(document.querySelector('#student-ssn'), 'keyup')
.pluck('srcElement', 'value')
.map(ssn => ssn.replace(/^\s*|\s*$|\-/g, ''))
.filter(ssn => ssn !== null && ssn.length === 9)
.subscribe(validSsn => {
console.log(`Valid SSN ${validSsn}`);
});

Why JavaScript?

为什么要学习JavaScript?因为它已经无处不在,几乎可以开发各种类型的应用。
为什么JavaScript可以实现函数式编程?因为它支持高阶函数、闭包、数组字面值等等。其实,函数是JavaScript的主要工作单位,这意味这函数不仅可以驱动应用的行为,也可以来定义对象、模块和处理事件。

1)函数式编程VS面向对象编程


OOP侧重于创建继承关系,并把方法和数据紧紧的绑在一起。而函数式编程偏向于多态函数,支持不同的数据类型,避免使用this

举个例子,定义一个Personclass,再定义一个Studentclass继承自Person。现在给定一个person实例,找到跟他在同一个国家的所有朋友。给定一个student实例,找到其他同学跟他在同一个国家并且在一个学校的同学。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
//略去对Person类和Student类的定义了

//面向对象编程的做法
// Person class
peopleInSameCountry(friends) {
var result = [];
for (let idx in friends) {
var friend = friends [idx];
if (this.address.country === friend.address.country) {
result.push(friend);
}
}
return result;
};

// Student class
studentsInSameCountryAndSchool(friends) {
var closeFriends = super.peopleInSameCountry(friends); //使用super调用父类的方法
var result = [];
for (let idx in closeFriends) {
var friend = closeFriends[idx];
if (friend.school === this.school) {
result.push(friend);
}
}
return result;
};

//创建一组Student实例
var curry = new Student('Haskell', 'Curry',
'111-11-1111', 'Penn State');
curry.address = new Address('US');

var turing = new Student('Alan', 'Turing',
'222-22-2222', 'Princeton');
turing.address = new Address('England');

var church = new Student('Alonzo', 'Church',
'333-33-3333', 'Princeton');
church.address = new Address('US');

var kleene = new Student('Stephen', 'Kleene',
'444-44-4444', 'Princeton');
kleene.address = new Address('US');

//使用上面定在Student类中的方法找到符合要求的学生
church.studentsInSameCountryAndSchool([curry, turing, kleene]);
//-> [kleene]

//接下来用函数式编程来解决,把问题分解成更小的function
function selector(country, school) {
return function(student) {
return student.address.country() === country &&
student.school() === school;
};
}

var findStudentsBy = function(friends, selector) {
return friends.filter(selector);
};

findStudentsBy([curry, turing, church, kleene],
selector('US', 'Princeton'));

//-> [church, kleene]

2)保持JavaScript对象的不可变

JavaScript对象是高度动态的,你可以在任何时间点修改、添加或删除它的属性。这虽然可以用来做很多投机取巧的事情,但却跟维护中到大型的项目带来了极大的困难。要如何来克服JavaScript对象的可变性呢?

采用值对象模式(Value Object Pattern)

值对象说的是判断两个对象是否相等应该是对象的“值”相等,而不是它俩必须是同一个对象。个人理解值对象其实类似于C++或Swift中定义的不可变结构体。来看一下JavaScript中是怎样实现值对象的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function zipCode(code, location) {
let _code = code
let _location = location || ''
return {
code: function() {
return _code
},
location: function() {
return _location
},
fromString: function(str) {
let parts = str.split('_')
return zipCode(parts[0], parts[1])
},
toString: function() {
return _code + '-' + _location
}

}
}

const princetonZip = zipCode('08544', '3345')
princetonZip.toString()

上面的做法,通过返回一组方法给调用者,保证了zipCode能够被访问到,却无法被修改,间接把_code_location变成了私有属性。再来看一个通过返回copy的对象实现不可变性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function coordinate(lat, long) {
let _lat = lat
let _long = long
return {
latitude: function() {
return _lat
},
longitude: function() {
return _long
},
translate: function(dx, dy) {
return coordinate(_lat + dx, _lat + dy) //返回一个copy对象
},
toString: function() {
return '(' + _lat + ',' + _long + ')'
}
}
}

const greenwich = coordinate(51.4778, 0.0015)
greenwich.toString() //-> (51.4778,0.0015)
greenwich.translate(10, 10).toString() //-> (61.4778,61.4778)

使用Object.freeze()防止对象状态被修改

1
2
var person = Object.freeze(new Person('Haskell', 'Curry', '444-44-4444'))
person.firstname = 'Bob' //这句话会导致报错,属性不允许被修改

但是在对象嵌套的情况下,被嵌套的对象的属性此时还是可以修改的。

解决办法就是手动对所有嵌套的对象都使用Object.freeze():

1
2
3
4
5
6
7
8
9
10
11
12
13
var isObject = (val) => val && Object.prototype.toString.call(val) == "[object Object]"

function deepFreeze(obj) {
if (isObject(obj) && !Object.isFrozen(obj)) {
Object.keys(obj).forEach(name => deepFreeze(obj[name]))
Object.freeze(obj)
}
return obj
}

module.exports = {
deepFreeze: deepFreeze
}

使用lenses修改对象

对于对象类型,必不可少的要对其属性进行set,在OOP中一旦调用set对象的状态必然被修改。为了防止被修改,可以向下面这样做:

1
2
3
set lastname(lastname) {
return new Person(this._firstname, lastname, this._ssn)
}

但是我们要给对象的所有属性都这样写,是一件相当令人崩溃的事,这时候就可以用到业界称之为的lenses的技术了。lenses简单的说就是函数式的getter/setter,现有的一些框架已经做了封装,不需要我们再一点点来实现。这本书中用到的是Ramda

1
2
3
4
5
6
7
8
var person = new Person('Alonzo', 'Church', '444-44-4444')
var lastnameLens = R.lenseProp('lastName')
//读属性值
R.view(lastnameLens, person) //-> 'Church'
//设置属性值
var newPerson = R.set(lastnameLens, 'Mourning', person)
newPerson.lastname //-> 'Mourning'
person.lastname //-> 'Church'

因为lenses实现了不可变的setters,所以在改变嵌套对象时仍能返回一个新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//补上person的address
person.address = new Address(
'US', 'NJ', 'Princeton', zipCode('08544','1234'),
'Alexander St.')

//创建一个`lens`获取address.zip
var zipPath = ['address', 'zip']
var zipLens = R.lens(R.path(zipPath), R.assocPath(zipPath))
R.view(zipLens, person) //-> zipCode('08544','1234')

//重新设置zipCode,会返回一个新的person
var newPerson = R.set(zipLens, person, zipCode('90210', '5678'))
R.view(zipLens, newPerson) //-> zipCode('90210', '5678')
R.view(zipLens, person) //-> zipCode('08544', '1234')
newPerson !== person

3)函数

成全JavaScript非常适合编写函数式程序的两个重要特性是:函数是一等公民和高阶函数。

函数是一等公民

函数作为一等公民(first-class)意味着:

  • 可以把匿名函数或lambda表达式赋值给一个变量
1
2
3
4
5
var square = function (x) {
return x * x
}

var square = x => x * x
  • 赋值给对象属性
1
2
3
var obj = {
method: function (x) { return x * x }
}
  • 函数也可以通过构造器被实例化
1
2
3
4
var multiplier = new Function('a', 'b', 'return a * b')
multiplier(2, 3)

//在JavaScript中,每个函数都是Function类型的实例

高阶函数

当函数作为参数被传入,或作为返回值被返回时,这样的函数就叫做高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//演示函数作为参数被传入
function applyOperation(a, b, opt) {
return opt(a, b)
}

var multiplier = (a, b) => a * b

applyOperation(2, 3, multiplier) //-> 6

//演示函数作为返回值被返回
function add(a) {
return function (b) {
return a + b
}
}

add(3)(3) //-> 6

刚接触这块时可能有点绕,始终要牢记一点:在一个函数后面写了(),那这个函数就被立即执行了。

函数的调用类型

说一说函数以不同方式被调用时,this指代的上下文:

  • 作为全局函数——this指代的是全局对象或undefined(在严格模式下)
1
2
3
4
5
function doWork() {
this.myVar = 'Some Value'
}

doWork() //调用全局函数doWork(),使得this指向全局对象
  • 作为方法——this指代的是方法的拥有者,这是JavaScript的面向对象性质重要的一部分。
1
2
3
4
5
6
var obj = {
prop: 'Some property',
getProp: function() { return this.prop }
}

obj.getProp() //`this`指代的是obj
  • 作为构造器用new来调用——this指代的是新被创建出来的对象。
1
2
3
4
5
function MyType(arg) {
this.prop = arg
}

var someVal = new MyType('some argument')

this是典型的面向对象的产物,它的不确定性使得代码很难理解,在函数式编程中应尽量避免使用this

函数方法

函数方法就是像callapply这样可以调用函数的方法,它们使得能够从一个已有的函数创建出新的函数。两个方法使用类似,call第二个参数为参数列表,apply第二个参数为数组。

1
2
Function.prototype.apply(thisArg, [argsArray])
Function.prototype.call(thisArg, arg1,arg2,...)

thisArg为null时,被调用的函数的context为全局对象。当thisArg不为null时,context就是thisArg指向的对象。举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function negate(func) {
return function() {
//执行传入的func,此时func的context为全局对象
return !func.apply(null, arguments)
}
}

function isNull(val) {
return val === null
}

var isNotNull = negate(isNull)

isNotNull(null) //-> false
isNotNull({}) //-> true

4)闭包和作用域

闭包

一个闭包的构成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//声明全局变量
var outerVar = 'Outer'
function makeInner(params) {
//声明局部变量
var innerVar = 'Inner'
//声明inner: innerVar和outerVar是inner闭包的一部分
function inner() {
console.log(`I can see: ${outerVar}, ${innerVar}, and ${params}`)
}
return inner
}
//调用makeInner返回inner函数
var inner = makeInner('Params')
inner() //-> 'I can see: Outer, Inner, and Params'

作用域

JavaScript中,当你定义一个变量,这个变量的作用域要么是全局作用域(global scope),要么属于函数作用域(function scope)。

JavaScript没有块级作用域(pseudo-block scope),所以一个变量要么是全局变量(定义在任何函数之外),要么是局部变量(定义在函数内,属于该函数)。

JavaScript的作用域工作机制是这样的:

  1. 检查变量的function scope。
  2. 如果不是局部变量,会再向外检查lexical scope,直到搜索到global scope。
  3. 如果变量没有被引用,JavaScript就会返回undefined。
1
2
3
4
5
6
7
8
9
var x = 'Some value';
function parentFunction() {
function innerFunction() {
console.log(x);
}
return innerFunction;
}
var inner = parentFunction();
inner();

对于上面变量x,查找过程如下:

关于函数的作用更具体的介绍可以看一下这篇文章:http://creeperyang.github.io/2015/01/JavaScript-dynamic-scope-vs-static-scope/

闭包的实际应用

  • 模拟私有变量

这个在zipCodecoordinate例子中已经展示过了,返回一组变量的get方法。

  • 使用IIFE创建Module Pattern

IIFE有两种形式:

1
2
(function(){ /* code */ }())  //括号内的表达式代表函数立即调用表达式
(function(){ /* code */ })() //括号内的表达式代表函数表达式

关于JavaScript的IIFE的具体分析介绍可以看这篇文章:https://segmentfault.com/a/1190000003985390

利用IIFE我们就可以来实现Module Pattern,Module Pattern将处理一个功能的代码快紧密的封装在一起,只暴露出需要给别人使用的变量和方法。一个封装良好的Module,具有可维护性,可复用性,还能避免命名空间的污染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var MyModule = (function (my) {

my.publicProperty = 'I am a public property'

let _privateProperty = 'Hello World'

my.method1 = function() {
console.log(_privateProperty)
}

my.method2 = function() {

}
return my
}(MyModule || {})) //

MyModule.method1()
MyModule.method2()

关于JavaScript的Module Pattern的详细介绍可以看这篇文章:https://www.cnblogs.com/Alex--Yang/p/3505285.html

  • 进行异步的服务器端调用

就是高阶函数作为回调(callback)被使用:

1
2
3
4
5
6
7
8
9
getJSON('/students',
(students) => {
getJSON('/students/grades',
grades => processGrades(grades),
error => console.log(error.message))
},
(error) =>
console.log(error.message)
)
  • 创建块级作用域变量

可以用闭包将循环体进行封装,避免暴露循环过程中的计数器等:

1
2
3
arr.forEach(function(elem, i) {
...
})

转载请注明出处:

作者:意林

原文链接:http://www.shinancao.cn/2019/04/01/Functional-JavaScript-Part1

–End–