javascript函数式编程,读了这篇你就会了

什么是函数式编程

首先,Javascript可以进行函数式编程,因为JavaScript中的函数就是第一类公民。这意味着变量可以做的事情函数同样也可以。ES6标准中还添加了不少语言特性,可以帮助用户更充分地使用函数式编程技术,其中包括 箭头函数Promise对象和 扩展运算符 等。
Javascript中,函数可以表示应用程序中的数据。细心的读者应该已经发现,可以使用关键字var像声明字符串数字或者其他任意变量那样声明函数

var log =function(message){
	console.log(message)
};
log("In JavaScript functions are variables")
// In Javascript,functions are variables

ES6规范下,我们可以使用箭头函数编写同样的函数。函数式程序员会编写大量的小型函数,使用箭头函数会方便很多:

const log = message =>console.log(message)

因为函数就是变量,我们可以将它们添加到对象中:

const obj = {
	message:"They can be added to objects like variables",
	log(message){
		console.log(message)
	}
}
obj.log(obj.message)
// They can be added to objects like variables

这些语句的效果殊途同归:它们都将一个函数存储到了一个名为1og的变量中。此外,关键字 const 被用来声明第二个函数,主要的目的是防止该函数被重写
在JavaScript中,我们还可以将函数添加到数组中:

const messages=[
	"They can be inserted into arrays",
	message => console.log(message),
	"like variables",
	message => console.log(message)
]

messages[1](messages[0]) //They can be inserted into arrays
messages[3](messages[2j) //like variables

函数可以像其他变量那样,作为其他函数的参数进行传递:

const insideFn = 1ogger =>
logger("They can be sent to other functions as arguments");

insideFn(message=>console.log(message))
// They can be sent to other functions as arguwents

我们可以说JavaScript就是函数式编程语言,因为它的函数是第一类成员。这意味着
函数就是数据。它们可以像变量那样被保存检索或者在应用程序内部传递

命令式和声明式

函数式编程还是更广义编程范式的一部分:声明式编程。声明式编程是一种辅程员格,采用读风格的应用程序代码有一个比较突出的特点,那就是对执行结果的描述远胜于执行过程
为了加深对声明式编程的理解,我们将会把它和命令式编程进行对比,该编程风格的特点是,其代码重点关注的是达成目标的具体过程。接下来以一个比较常见的任务为
例:让字符串兼容URL格式。一般来说,这可以通过连字符替换字符串中的所有空格实现,因为空格对URL地址的兼容性不佳。首先,我们使用命令式编程风格完成此任务:

var string="This is the midday show with Cheryl Maters";
var urlFriendly="";
for (var i=0; i<string.length; i++){
	if(string[i]===""){
		urlFriendly +="-"}else {
	urlFriendly += string[i];
	}
}
	console.log(urlFriendly);

从该程序的结构来看,它只关注如何完成这样一个任务。我们使用了for循环和i
语句,并使用同等的运算符进行赋值。单独看这些代码并不能告诉我们更多信息。
命令式编程风格需要辅以大量注释说明帮助用户理解它的具体用途
现在我们使用声明式编程风格来解决同样的问题:

const string ="This is the mid day show with Cheryl waters"
const urlFriendly = string.replace(//g,"-")
console.log(urlFriendly)

使用string.replace方法是一种说明可能会发生什么的方式:字符串中的空格将
全被替换。如何处理空格的细节被抽象封装到了rep1ace函数内部。在一个声明武程
序中,语法本身描述了将会发生什么相关的执行细节被隐藏了。
声明式程序易于解释具体用途,因为其代码本身就描述了将会发生什么。

函数式编程基本概念

现在读者已经了解了函数式编程,以及“函数”和“声明式”的意义接下来我们将
继续介绍函数式编程的核心概念:不可变性纯函数数据转换高阶函数递归

1.保持是数据的不可变性,
2. 确保尽量使用纯函数,只接受一个参数,返回数据或者其他函数
3. 尽量使用递归处理循环(如果有可能的话)

不可变性

不可变性就是指不可改变。在函数式编程中,数据是不可变的,它们水远无法修设。在不修改原生数据结构的前提下,我们在这些数据结构的拷贝上进行编辑,并使用它们取代原生的数据。
为了了解不可变性的工作机制,让我们看看它是如何修改数据的。现在来考察一个表示颜色的对象:

let color_lawn={
	title:"lawn",
	color:"#OOFF0o",
	rating:0
}

function ratecolor(color,rating){
	color.rating = rating
	return color
}

console.log(ratecolor(color_lawn,5).rating) //5
console.log(color_lawn.rating)  //5

在Javascript中,函数参数会被指向实际的数据。像这样设置颜色的评分是一种比较
槽糕的做法,因为它修改异化了原来的颜色对象,我们可以重写颜色评分函数从而达到不破坏原生物(颜色对象)的目的:

1.0bject.assign
var ratecolor = function(color,rating){
	return Object.assign({},color,{rating:rating})
}
console·log(ratecolor (color_1awn,5).rating) //5
console.log(color_lawn.rating) //4

这里我们使用0bject.assign方法修改颜色评分。0bject.assign方法是一种拷贝机制

2.扩展运算符

我们可以使用ES6规范下的箭头函数和ES7规范下的对象扩展运算符编写同样的函
数。rateColor函数使用扩展运算符将颜色对象拷贝到一个新对象中,然后重写它的评分:

const rateColor=(color,rating)=>
({
	...color,
	rating
})

采用了JavaScript新版本语法特性的rateColor函数几乎和上一个函数一样。它将颜色对象视为一个不可变对象,这样一来用到的语法更少,而且可看起来更简洁一些。

3.Array.concat
let list = [
	{ title:"Rad Red"},
	{ title:"Lawn"},
	{ title:"Party Pink"}
]
const addcolor (title,array)=> array.concat({title})
console.log(addcolor("Glam Green",1ist).length) //4
console.log(list.length) //3

Array.concat方法会将数组串联起来。这种情况下,它会生成一个包含 新的 颜色标题的对象,并将它添加到原生数组的副本上

用户还可以使用ES6的扩展运算符串联数组,同时该操作符可以使用同样的机制拷贝对象。这里使用了JavaScript的新语法,其效果和前面的addcolor 函数是等价的:

const addcolor=(title,list)=>[....list,{title}]
存函数

纯函数是一个返回结果只依赖于输入参数的函数不会产生副作用不修改全局变量,或者任何应用程序的State。它们将输入的参数当作不可变数据。
存函数的核心概念:

  1. 函数应该至少接收一个参数
  2. 函数应该返回一个值或者其他函数
  3. 函数不应该修改或者影响任何传递给它的参数

为了理解纯函数,接下来看一个非纯函数:

var frederick={
	name:"Frederick Douglass",
	canRead:false,
	canwrite:false
}
function selfEducate(){
	frederick.canRead = true
	frederick.canwrite = true
	return frederick
}
selfEducate()
console.log( frederick)
//{name:"Frederick Douglass",canRead:true,canwrite:true}

函数selfEducate不是一个纯函数。它并没有接收任何参数,并且也没有返回一个值
或者函数。它还修改了其作用域之外的变量:Frederick。一旦selfEducate函数被执
行后,“世界”就发生了变化。它产生了副作用。
改写:

var frederick={
	name:"Frederick Douglass",
	canRead:false,
	canwrite:false
}

const selfEducate =person =>
	({
		...person,
		canRead:true,
		canwrite:true
	})
console.log( selfEducate(frederick))
console.log( frederick)
//{name:"Frederick Douglass",canRead:true,canwrite: true}
//{name:"Frederick Douglass",canRead:false,canMrite:false)

最后,这个版本的se1fEducate成了一个纯函数。它的返回值是根据传递给它的参数
生成的:person。它在不改变传递给它的参数的情况下,返回了一个新的person对象,因此不会产生副作用。

数据转换

如果数据是不可变的,那么应用程序的内部如何进行状态转换的呢,函数式编程的做法是将一种数据转换成另一种数据,我们使用函数生成转换后的副本,这些函数使得命令式的代码更少,并且大大的降低了复杂度。
为了充分利用javascript中的函数式特性,下面两个核心函数是必须要掌握的:Array.mapArray.reduce
Array.map 例1:

const schools= [
	"Yorktown",
	"Washington & Lee",
	"Wakefield"
]
//将 Hign School 追加到每个学校名字的后面
const highschool = schools.map(school=>({name:school + 'Hign School'}))

// Yorktown Hign School
// Washington & Lee Hign School
// Wakefield Hign School

//schools:不会被改变,完好无损
// Yorktown 
// Washington & Lee 
// Wakefield 

Array.map 例2:
如果用户需要创建一个纯函数来修改对象数组中的某个对象,m3p函数也能胜任这项工作。在接下来的示例中,我们将会在不改变数组schools 的情况下,将其中的“Stratford”改为“HB Woodlawn",这些变量是在更新的数组上进行的,并不会对原有的数组产生影响:

let schools =[
	{name:"Yorktown"},
	{name:"Washington & Lee"},
	{name:"Wakefield"},
	{name:"Stratford"}
]
let updatedSchools = editName("Stratford","HB Woodlawn",schools)

const editName =(oldName,name,arr)=>
	arr.map(item =>{
		if (item.name === oldName){
			return {
			...item
			name
			}
		}else {
			return item
		}
	})
console.log( updatedschools[3])//{ name:"HB Woodlawn"},
console.log( schools[3]) //{ name:"Stratford"}, 没有影响

如果用户需要将一个数组转换成一个对象,那么用户可以通过Array.map搭配0bject.keys一起使用来达到上述目的。0bject.keys法可以用来获得某个对象中属性键的数组。
比如我们可以将schools对象转换成schools的数组:

const schools ={
	"Yorktown":10,
	"Mashington & Lee":2.
	"Wakefieid":5
}
const schpolAray = Object.keys(schools ).map(key=>
	({
		name:key,
		wins:schools[key]
	})
)
console.log(schoolArray)
//[
//	{
//		name:"Yorktown",
//		wins:10
//	},
//	{
//		name:"Mashington & Lee",
//		wins:2
//	},
//	{
//		name:"Wakefieid",
//		wins:5
//	}
//]

reduce函数可以用来将数组转换成任意值,比如数字、字符串、布:
值、对象,甚至是函数。
接下来的示例将会演示如何在一个数字数组中找出最大值。我们需要将一个数组转成数字,为此,可以使用reduce方法:

const ages =[21,18,42,40,64,63,34]const maxAge = ages.reduce((max,age)=>{
	console.1og(${age}>${max}=${age >max}`)if(age>max){
		return age
	}else {
		return max
	}
}0)
console.1og('maxAge',maxAge);

//21>0=true
//18>21=false
//42>21=true
//40>42=talse
//64>42=true
//63>64=false
//34>64=false
// maxAge 64
高阶函数

高阶函数的使用对于函数式编程也是必不可少的,第一类高阶函数是将其他函数当作参数传递的函数。Array.mapArray.filterArray.reduce都可以将函数当作参数进行传递,所以它们都是高阶函数。

接下来看看如何实现一个商阶函数。在下列示例中,我们将创建一个名为invokeIf
的回调函数当条件经过测试为true时将会调用一个回调函数;当条件经过测试为
false时,将会调用另外一个回调函数;

const invokeIf =(condition,fnTrue,fnFalse)=>
	(condition)?fnTrue():fnFalse()
const showwelcome=()=>
	console.log("Welcome!!!")
const showlnauthorized =()=>
	console.1og("Unauthorized!!!")
invokeIf(true,showwelcome,showUnauthorized) //"Welcome"
invokeIf(false,showwelcome,showUnauthorized) //"Unauthorized"

柯里化(CurTying)是一种采用了高阶函数的函数式编程技巧。柯里化实际上是一种
将某个操作中已经完成的结果保留,直到其余部分后续也完成后可以一并提供的机
制。这是通过在一个函数中返回另外一个函数实现的,即柯里函数。
柯里化函数举例:

// 实现一个add方法,使计算结果能够满足如下预期:( 一道经典面试题 )
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;

function add() {
    // 第一次执行时,定义一个数组专门用来存储所有的参数
    var _args = Array.prototype.slice.call(arguments);

    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var _adder = function() {
        _args.push(...arguments);
        return _adder;
    };

    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    _adder.toString = function () {
        return _args.reduce(function (a, b) {
            return a + b;
        });
    }
    return _adder;
}

add(1)(2)(3)                // 6
add(1, 2, 3)(4)             // 10
add(1)(2)(3)(4)(5)          // 15
add(2, 6)(1)                // 9
递归

递归是用户创建的函数调用自身的一种技术,在解决实际问题涉及到循环时,递归函数可以提供一种替代性的方案,下面的函数我们可以使用for循环实现它,当然也可以使用递归实现:

const countdown = (value,fn)=>{
	fn(value)
	return (value>0)? countdown(value-1,fn) : value
}
countdown(6,value=>console.log(value))
//6
//5
//4
//3
//2
//1
//0

递归的另一个好处就是很好的处理异步过程的函数式编程技术,函数调用自身也能实现函数的延时调用

const countdowm = (value,fn,delay=1000)=>{
	fn(value)
	return (value>0)?
		setTimeout(()=>countdowm(value-1.fn),delay):
		value
}
const log = value =>console.log(value)
countdown(6,log)
//下面的数字每隔 一秒 输出
//6
//5
//4
//3
//2
//1
//0

猜你喜欢

转载自blog.csdn.net/weixin_48786946/article/details/106974273