Java教程

【JavaScript】函数式编程

本文主要是介绍【JavaScript】函数式编程,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

0. 什么是函数式编程?

函数式编程(Functional programming)是一种编程范式,利用函数把整个程序的执行过程封装起来,通过组合各种函数来获得程序的执行结果。

举个例子,这个例子是将一段英文文本转换为每个单词的首字母均大写(对应Python中的.title()方法):

let str = "My name is UltraMar1ne, I've 18 years old."
str = str.split(" ")
	.map(word => word.slice(0, 1).toUpperCase() + word.slice(1))
	.join(" ");
console.log(str)

这段代码的执行结果是:

My Name Is UltraMar1ne, I’ve 18 Years Old.

解读一下上面的代码:

先通过.split()方法将文本分割成单词数组;然后通过.map()方法返回一个新的数组,这个新数组的每个单词通过.toUpperCase()方法将切片后的第一个字符大写;最后再将其通过.join()方法用空格将其拼接

通过上面的例子,我们仔细观察一下,可以发现函数式编程有以下特点:

  • 通过函数进行数据转换
  • 串联多个函数求取最终结果

1. 声明式编程和命令式编程

首先来解释一下这两个概念:

  • 声明式编程:告诉程序我们想干什么,而不是通过自己手写操作来一步步实现。这种程序一般是表达式,是一些函数调用的复合、一些值和操作符,用来计算结果
  • 命令式编程:编写指令,来完成自己所想的内容。会涉及很多复杂的细节,比如if, while, for

举个例子,现有一系列自定义对象,其中一个的定义如下:

let student = {
    name,
    age,
    studentId,
    score,
    ...
};

然后我想取出所有对象的studentId属性,两种编程方式分别如何去做呢?(这里将以返回值的形式展现)

// 声明式编程
let getStudentId = (arr) => {
    return arr.map(student => student.studentId);
}

// 命令式编程
let getStudentId = (arr) => {
    let res = [];
    arr.forEach(student => {
        res.push(student.studentId);
    });
    return res;
}

在规模较小、较为简单的程序中,两者其实并没有什么太大的差别。

但是随着程序规模的增长,函数式编程采用表达式的思想将会贯穿代码始终,这也是函数式编程的基础。

2. 函数式编程的特性

2.1 无副作用

调用函数时不会修改外部状态

即,无论调用多少次,其返回值状态不可能会发生改变

let a = 1;
// 含副作用,a被改变了
function add() {
    return ++a;
}

// 无副作用
function add(a) {
    return a+1;
}

2.2 透明引用

函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量

let a = 1, b = 2;
// 使用的并不是自己的变量,容易造成污染
function add() {
    return a+b;
}

// 内部使用到的变量
function add(a) {
    return a+1;
}

2.3 不可变变量

一个变量一旦创建后,就不能再进行修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全。多个线程可以同时访问同一个不可变变量,让并行变得更容易实现。 由于 JavaScript 原生不支持不可变变量,需要通过第三方库来实现。 (如 Immutable.jsMori 等等)

let obj = Immutable({ a: 1 });
let obj2 = obj.set('a', 2);
console.log(obj);  // Immutable({ a: 1 })
console.log(obj2); // Immutable({ a: 2 })

2.4 函数是一等公民

在JavaScript中,函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值

3. 常见的函数式编程模型

3.1 闭包

如果一个函数引用了自由变量,那么该函数就是一个闭包

自由变量是指不属于该函数作用域的变量(所有全局变量都是自由变量,严格来说引用了全局变量的函数都是闭包,但这种闭包并没有什么用,通常情况下我们说的闭包是指函数内部的函数)

闭包形成需要什么条件呢?

  • 存在内、外层函数嵌套
  • 内层函数对外层函数的局部变量进行引用

闭包有什么用呢?

闭包可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者计算的中间量

举个例子:

// 简单的缓存工具
// 匿名函数创造了一个闭包
const cache = (function() {
  const store = {};
  return {
    get(key) {
      return store[key];
    },
    set(key, val) {
      store[key] = val;
    }
  }
}());

console.log(cache) //{get: ƒ, set: ƒ}

cache.set('a', 1);
cache.get('a');  // 1

上述例子中,创建了一个存储对象store,由于在返回时形成了闭包,这样使得store可以一直被引用,不会被回收

由此也可见闭包的弊端:持久化变量不会被回收,导致内存占用,很容易造成资源浪费

3.2 高阶函数

高阶函数这个概念自我从Python学起时,就一直有被提及,当时还觉得就是很高级的函数,其实不然

高阶函数是指一个函数以函数为参数,或以函数为返回值,或同时存在上述两种情况

高阶函数经常用于:

  • 抽象或隔离行为、作用,异步控制流程作为回调函数,promises,monads等;
  • 创建可以泛用于各种数据类型的功能;
  • 部分应用于函数参数(偏函数应用)或创建一个柯里化的函数,用于复用或函数复合;
  • 接受一个函数列表并返回一些由这个列表中的函数组成的复合函数。

JavaScript是支持高阶函数的,而且也自带了一些我们熟知的高阶函数如:Array.prototype.map, Array.prototype.filter, Array.prototype.reduce等,具体用法请移步红宝书(《JavaScript高级程序设计(第4版)》)

3.3 函数柯里化

函数柯里化又称部分求值,柯里化函数会接收一些参数,但是不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值

举个例子:

// 普通函数
function add(x,y){
    return x + y;
}
add(1,2); // 3

// 函数柯里化
let add = function(x) {
  return function(y) {
    return x + y;
  };
};

let increment = add(1);
increment(2); // 3

// 也可以如此调用
let increment = add(1)(2); // 3

3.4 函数组合(Composition)

上文有提到过,可以通过函数的链式调用来进行表达式的求取。但是,随着链式调用的规模不断增大,代码的可读性也会随之降低,有没有一种方法可以解决上述问题?

有,就是函数组合

例如,假设有一个 compose 函数,它可以接受多个函数作为参数,然后返回一个新的函数。当我们为这个新函数传递参数时,该参数就会「流」过其中的函数,最后返回结果:

// 两个函数的组合
let compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};

// 或者
let compose = (f, g) => (x => f(g(x)));
let add1 = x => x + 1;
let mul5 = x => x * 5;
compose(mul5, add1)(2);// =>15
这篇关于【JavaScript】函数式编程的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!