本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988…
本文阅读指数:3
函数式编程随有一定的市场,但是目前来看也是说得多做得少。本文帮助你理解一些函数式编程的基本概念。
概述
函数式编程,简单来说就是一个函数,入参是数据,出参是函数。
函数式编程最美妙的地方,是它不会改变我们的入参,也不会产生任何的副作用。状态值是通过函数来表达的。
这一章我们会讨论函数式编程在JS中是如何工作的,以及相关的一些重要概念。
面向对象的JS
首先,我们知道JS是基于原型的语言,不是基于类的语言,这种模型经常让开发者困惑。基于类的语言,比如JAVA ,C#的等,主要有两个概念:类和实例。类定义了对象所有的属性,而实例就是将类实例化了。
JS的面向对象风格,只是看起来像,但实际上并不是。表面上看你用的是类语法,但是实际上还是原型
比如:
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
speak() {
return `Hello, my name is ${this.name} and I am ${this.age}`
}
}
注意Person类构造函数有两个参数。关键字this指向我们的原型,Person。 我们的方法speak() 可以这么使用:
let victor = new Person('Victor', 23);
console.log(victor.speak());
new Person会调用构造函数,传入的参数就会被设置到victor 对象。 speak() 函数会被添加到构造函数的原型上。
我们也可以在Person类之外创建一个新类,扩展它里面的属性和方法
let Work = class extends Person {
constructor(name, age, work) {
super(name, age);
this.work = work
}
getInfo() {
return `Hey! It's ${this.name}, I am age ${this.age} and work for ${this.work}.`
}
}
let alex = new Work('Alex', 30, 'SessionStack');
console.log(alex.getInfo());
我们创建了一个类,继承自Person,并且定义了更多的属性。在基于类的语言中,它被称为子类。
如果使用旧版本的JS该怎么做呢?记住,JS中的类语法实际依然是基于原型的。我们看看发生了什么:
let Person = function(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.speak = function() {
return `Hello, my name is ${this.name} and I am ${this.age}`
}
let victor = new Person(`Victor`, 23)
console.log(victor.speak())
上面的代码,我们调用了Person() 函数,注意前面还有个关键字new。new 改变了函数的上下文,所以它被称为构造器。它生成了一个Person 对象的实例,并赋值给victor。 像上面那样实现对Person的继承,我们可以这么做:
let Work = function(name, age, work) {
Person.call(this, name, age);
this.work = work;
}
注意我们没有使用 super() 关键字,因为JS中是不存在的。call() 接受了this和其他参数,然后把Person的属性再添加到Workshang 。.call()方法,连接了Person的构造器和Work类。换句话说,我们从Person 中借走了属性,并且添加到Work中。
在原型中,也没有extend这样的语法。但是可以这么连接Work 和 Person
Object.setPropertyOf(Work.prototype, Person.prototype);
这样Work就可以使用Person的属性了。给work添加方法的话可以这么做:
Work.prototype.getInfo = function() {
return `Hey! It's ${this.name}, I am age ${this.age} and work for ${this.work}.`
}
let alex = new Work("Alex", 40, 'SessionStack');
console.log(alex.getInfo());
我们需要知道JS是基于原型实现的对象型语言,我们在ES6中使用class 语法,只是一个语法糖。
有了上面的基础,现在来看看JS中的函数式编程:
什么是函数式编程
在JS中使用函数式编程会容易一点,但是不是对所有开发者都适用。因为JS本身是一个基于原型的语言,它里面的'运行继承’, ‘this’, ‘setPropertyOf’ 已经很让人迷惑了。
不过,跟使用错误的‘this’ banding相比, JS中的函数式方式会让事情变得简单一点,也更好维护一点。 使用函数式编程的社区成员还是很多的,在你遇到问题的时候,求助StackOverflow会得到很多帮助。
看一个简单的函数式代码:
const sayHello = function(name) {
return `Hello ${name}`;
}
sayHello('Victor');
# => Hello Victor
我们声明一个函数,有一个参数 name。非常的简单明了。
但是函数式编程会复杂一点,我们要先理解一些概念:纯函数,高阶函数,不可变性,头等函数等等,
JS中函数式编程概念
纯函数
函数式编程的一个主要目标使用纯函数,避免副作用。无副作用的意思就是你的函数逻辑只能跟传入参数有关。我们先看一个有副作用的非纯函数,
let surname = 'Jonah';
const sayHi = function() {
return `Hi ${surname}`;
}
上面的代码中,没有输入参数,但是却使用了一个函数之外的全局变量。这就可能引起副作用。
将其改造为纯函数:
const sayHi = function(surname) {
return `Hi ${surname}`;
}
现在函数只关心输入参数,并处理了它。这就是纯函数。 这就是函数式编程最重要的概念。 除此之外还有一些小概念要知道
高阶函数
高阶函数也是函数,它的入参是函数,出参还是函数,要记住函数也是值,可以被传递来传递去的。例如:
const getSum = function(num) {
return num + num;
}
getSum(9);
我们创建了一个函数,并赋值给getSum, 我们还可以把它传递给另一个变量,比如:
const addNum = getSum;
addNum(9);
可以看到,函数可以被当做变量那样传递。因此,也可以被当成是参数传递给高阶函数。
我们可以持续传递函数到另一个函数中,让我们把很多小函数组合成一个大函数。这被称为组合。
function a(x) {
return x * 2;
}
function b(x) {
return x + 1;
}
function c(x) {
return x * 3;
}
const d = c(b(a(2)));
console.log(d) // 15
我们可以传递每一个函数返回值到下一个函数中。 这也是我们使用高阶函数的原因,因为它使用组合,让我们的代码更干净
一个比较常用的高阶函数例子是filter() 方法。filter()需要传进一个函数,然后返回一个新的数组。例如:
function isLarge(value) {
return value > 10;
}
const dataArray = [10, 11, 12, 3, 4];
const filteredArray = dataArray.filter(isLarge);
console.log(filteredArray); // [11, 12]
回调函数必须返回一个布尔值,如果返回值是true,当前遍历的值就会被加入到新数组中, 类似的例子还有 map() 和 reduce() 方法。在开始下一节之前,先看看map()的方法:
function squareRoot(value) {
return Math.sqrt(value)
}
const dataArray = [4, 9, 16];
const mappedArray = dataArray.map(squareRoot);
console.log(mappedArray); // [2, 3, 4]
不可变性
函数式编程的另一个重要概述是拒绝异变。异变是指对状态和数据的改变。不可变是指原来的数据不能变,当发生变更时,需要设置一个新的对象。看一个具有异变性的代码:
let data = [1, 2, 3, 4, 4];
data[4] = 5;
console.log(data); // [1, 2, 3, 4, 5]
注意我们把1, 2, 3, 4, 4 改变成了1, 2, 3, 4, 5。这段代码可能引起一些Bug。假设这个数组在某处被使用,但是它其实已经改变了。那么JS中怎么处理不可变性?
const names = ["Alex", "Victor", "John", "Linda"]
const newNamesArray = names.slice(1, 3) // ["Victor", "John"]
源数组names 没有改变,然后生成了一段新数据。
使用Object.freeze() 可以让对象也不可变。这个方法冻结了对象,不允许该对象属性的增加和删除。
例如:
const employee = {
name: "Victor",
designation: "Writer",
address: {
city: "Lagos"
}
};
Object.freeze(employee);
employee.name = "Max"
//Outputs: Cannot assign to read-only property 'name'
//Checks if our object is immutable or not
Object.isFrozen(employee); // === true
这里,我们的对象就不可变了,并且不能与之交互。 这里也有一个问题,就是我们如果要改动数组就不得不一遍又一遍的弃用旧数组。如果数组中有成千上万的数据,那么带来的内存消耗和问题会很大。好在现在已经有一些库来帮我们处理这些问题,不如 immutable.js 和 Mori.
有了这些概念,我们已经知道JS 函数式编程的概念和优点了。可以看到我们的代码更易读了,
声明式和命令式
实现函数式编程,有两种途径:声明式和命令式。命令式是声明你要做的每一步。比如,如果你想做包子,那么命令式会是这样
- 和面
- 剁馅儿
- 包包子
- 蒸熟
而声明式只是说出来你想做的。例如,你想做包子,仅此而已,我们不需要去描述每一步。 不过这些只是简单的描述,告诉你两者的区别。
命令式:
这里我们会直接告诉计算机我们做什么事情
// Function to filter an array; return greater than 5 numbers
const filterArray = (array) => {
let filteredArray = [];
for(let i = 0; i < array.length; i++) {
if(array[i] > 5) {
filteredArray.push(array[i]);
}
}
return filteredArray;
}
const array = [1, 2, 3, 4, 5, 6, 7, 8]
filterArray(array)
我们没有告诉计算机我们想要什么,只是告诉了我们每一步要做的事情。 我们的步骤包括:
- 声明一个空数组
- 迭代给定数组
- 如果当前项大于5
- 如果大于5,就把匹配的项加入到之前声明的空数组中
- 显示我们的新数组
声明式:
// Filter method to give us a new array
const filterArray = array => array.filter(x => x > 5);
const array = [1, 2, 3, 4, 5, 6, 7, 8];
console.log(filterArray(array)); // [6, 7, 8]
声明式就很简单,我们声明了一个函数,然后让它去做我们想让它做的。
为什么大多数人推荐函数式编程
开发者选择使用函数式编程,有一些共同原因。同时刚学习函数式编程可能会踩坑,但是一旦你彻底理解了整个概念,就会变得很容易。 这里是一些主要原因:
- 让代码更加模块化;一个函数可以跟另一个完全不相干的函数合并。由于函数是可组合的,就像React组件那样,可以做到很好的复用。
- 调试更容易了:更少的Bug,更好的维护性。高阶函数易于调试,且代码量少,我们的代码就会更安全。
- 开发者更容易读懂你的代码。因为你是按照人类的思维来写的代码,而不是过程性的。
- 在函数式编程中,开发者倾向于写更加干净快速的代码。而不是迭代代码,像for循环那样。
结论
在这一章,我们总览了以下面向对象的JS工作方式,class语法和传统的基于原型的区别。
介绍了一些函数式编程的概念,帮助你理解使用函数式编程的原因。
没有更好或者更坏的编程模式。这个章节只是帮助你理解函数式编程的概念。