写在前面
这里是小飞侠Pan🥳,立志成为一名优秀的前端程序媛!!!
本篇文章收录于我的专栏:前端精进之路
同时收录于我的github前端笔记仓库中,持续更新中,欢迎star~
一、什么是函数式编程
介绍
函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。函数式编程意味着你可以在更短的时间内编写具有更少错误的代码。
常见特性
无副作用
指调用函数时不会修改外部状态,即一个函数调用 n 次后依然返回同样的结果。
var a = 1;
// 含有副作用,它修改了外部变量 a
// 多次调用结果不一样
function test1() {
a++
return a;
}
// 无副作用,没有修改外部状态
// 多次调用结果一样
function test2(a) {
return a + 1;
}
透明引用
指一个函数只会用到传递给它的变量以及自己内部创建的变量,不会使用到其他变量。
var a = 1;
var b = 2;
// 函数内部使用的变量并不属于它的作用域
function test1() {
return a + b;
}
// 函数内部使用的变量是显式传递进去的
function test2(a, b) {
return a + b;
}
不可变变量
指的是一个变量一旦创建后,就不能再进行修改,任何修改都会生成一个新的变量。使用不可变变量最大的好处是线程安全。多个线程可以同时访问同一个不可变变量,让并行变得更容易实现。 由于 JavaScript 原生不支持不可变变量,需要通过第三方库来实现。 (如 Immutable.js,Mori 等等)
var obj = Immutable({ a: 1 });
var obj2 = obj.set('a', 2);
console.log(obj); // Immutable({ a: 1 })
console.log(obj2); // Immutable({ a: 2 })
纯函数
如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入的参数(相同的输入,必须得到相同的输出)。
/ 纯函数
const calculatePrice=(price,discount)=> price * discount
let price = calculatePrice(200,0,8)
console.log(price)
// 不纯函数
const calculatePrice=(price,discount)=>{
const dt= new Date().toISOString()
console.log(`${dt}:${something}`)
return something
}
foo('hello')
函数副作用
//函数外a被改变,这就是函数的副作用
let a = 5
let foo = () => (a = a * 10)
foo()
console.log(a) // 50
let arr = [1, 2, 3, 4, 5, 6]
arr.slice(1, 3) //纯函数,返回[2,3],原数组不改变
arr.splice(1, 3) // 非纯函数,返回[2,3,4],原数组被改变
arr.pop() // 非纯函数,返回6,原数组改变
函数副作用可变性和不可变性
- 可变性是指一个变量创建以后可以任意修改
- 不可变性指一个变量,一旦被创建,就永远不会发生改变,不可变性是函数式编程的核心概念
// javascript中的对象都是引用类型,可变性使程序具有不确定性,调用函数foo后,我们的对象就发生了改变;这就是可变性,js中没有原生的不可变性
let data = { count: 1 }
let foo = data => {
data.count = 3
}
console.log(data.count) // 1
foo(data)
console.log(data.count) // 3
// 改进后使我们的数据具有不可变性
let data = { count: 1 }
let foo = data => {
let lily = JSON.parse(JSON.stringify(data)) // let lily= {...data} 使用扩展运算符去做拷贝,只能拷贝第一层
lily.count = 3
}
console.log(data.count) // 1
foo(data)
console.log(data.count) // 1
二、高阶函数
- 接受一个或多个函数作为输入
- 输出一个函数
JavaScript 语言中内置了一些高阶函数,比如 Array.prototype.map,Array.prototype.filter 和 Array.prototype.reduce,它们接受一个函数作为参数,并应用这个函数到列表的每一个元素。
Array.prototype.map
[1, 2, 3, 4]
const arr1 = [1, 2, 3, 4];
const arr2 = arr1.map(item => item * 2);
console.log( arr2 );
// [2, 4, 6, 8]
console.log( arr1 );
// [1, 2, 3, 4]
Array.prototype.filter
[1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]
const arr1 = [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4];
const arr2 = arr1.filter( (element, index, self) => {
return self.indexOf( element ) === index;
});
console.log( arr2 );
// [1, 2, 3, 5, 4]
console.log( arr1 );
// [1, 2, 1, 2, 3, 5, 4, 5, 3, 4, 4, 4, 4]
Array.prototype.reduce
[0, 1, 2, 3, 4]
// 使用高阶函数
const arr = [5, 7, 1, 8, 4];
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0);
console.log(sum)//25
三、函数柯里化
柯里化又称部分求值,柯里化函数会接收一些参数,然后不会立即求值,而是继续返回一个新函数,将传入的参数通过闭包的形式保存,等到被真正求值的时候,再一次性把所有传入的参数进行求值。
demo
// 普通函数
function add(x,y){
return x + y;
}
add(1,2); // 3
// 函数柯里化
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
increment(2);// 3
作用:
主要有 3 个作用: 参数复用、提前返回和 延迟执行
参数复用:我们可以传入部分参数,然后拿着返回的函数再加入新的参数,这样前面传入的参数就做到了参数复用。
提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。
实现
function currying(fn, ...args) {
const length = fn.length;
let allArgs = [...args];
const res = (...newArgs) => {
allArgs = [...allArgs, ...newArgs];
if (allArgs.length === length) {
return fn(...allArgs);
} else {
return res;
}
};
return res;
}
// 用法如下:
// const add = (a, b, c) => a + b + c;
// const a = currying(add, 1);
// console.log(a(2,3))
四、函数组合 (Composition)
假设有一个 compose 函数,它可以接受多个函数作为参数,然后返回一个新的函数。当我们为这个新函数传递参数时,该参数就会「流」过其中的函数,最后返回结果。
// 用法如下:
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a(1)); // 1+4+3+2+1=11
我们compose的作用就是将嵌套执行的方法作为参数平铺,嵌套执行的时候,里面的方法就是右边的方法最开始执行,然后往左边返回。
function compose(...fns){
return (arg) =>
fns.reduceRight((prev,fn) => {
return fn(prev)
},arg)
}
解释
...fnsreturn
return的这个函数传入的参数就是我们要计算的初始值。
注意:如果箭头函数的返回值只有一个式子,那么可以不使用{}和return。
在函数体的操作中,我们需要从右往左遍历所有的函数,prev是上一次返回的值,fn是当前处理的函数,并且要把arg当做初始值传入。
在函数体中,我们要返回当前fn函数的执行结果作为prev的值。
pipe函数
compose
function compose(...fns){
return (arg) =>
fns.reduce((prev,fn) => {
return fn(prev)
},arg)
}
五、偏函数
柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。
局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。
/*
用bind函数实现偏函数,bind的另一个用法使一个函数拥有预设的初始参数,将这些参数写在bind的第一个参数后,
当绑定函数调用时,会插入目标函数的初始位置,调用函数传入的参数会跟在bind传入的后面
*/
let add = (x, y) => x + y
let rst = add.bind(null, 1)
rst(2) //3
六、防抖与节流
防抖
你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。
function debounce(fn,delay){
//1.定义一个定时器,保存上一次的定时器
let timer = null
//2.真正执行的函数
return function(...args){
//取消上一次的定时器
if(timer){
clearTimeout(timer)
}
//延迟执行
timer = setTimeout(() => {
//执行外部传入的函数
fn.apply(this,args)
}, delay);
}
}
-
this绑定:真正执行的函数是要绑定我们返回的函数,而箭头函数没有this,它的this指向上层作用域,正好是我们返回的函数
-
参数传递:因为真正执行的函数是我们返回的函数,所以参数是放在返回的那个函数中,直接解构
验证
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<script>
function debounce(fn,delay){
//1.定义一个定时器,保存上一次的定时器
let timer = null
//2.真正执行的函数
return function(...args){
//取消上一次的定时器
if(timer){
clearTimeout(timer)
}
//延迟执行
timer = setTimeout(() => {
//执行外部传入的函数
fn.apply(this,args)
}, delay);
}
}
const inputEl = document.querySelector('input');
let counter = 0
const inputChange = function(){
console.log(`发送了第${++counter}次网络请求`)
}
inputEl.oninput = debounce(inputChange, 1000)
</script>
</body>
</html>
节流
1sonmousemove100onmousemove100ms1sonmousemove10
方法1: 定时器方式实现
缺点:第一次触发事件不会立即执行fn,需要等delay间隔过后才会执行
let throttle = (fn, delay) => {
let flag = false
return function(...args) {
if (flag) return
flag = true
setTimeout(() => {
fn(...args)
flag = false
}, delay)
}
}
方法2:时间戳方式实现
缺点:最后一次触发回调与前一次的触发回调的时间差小于delay,则最后一次触发事件不会执行回调
const throttle2 = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
+new Date()
js在某个数据类型前使用‘+’,这个操作目的是为了将该数据类型转换为Number类型,如果转换失败,则返回NaN;
+'2'+1 // 3
+[1] // NaN
+new Date() 会调用Date.prototype 上面的 valueOf方法
下面的例子返回效果等同:
+new Date();
new Date().getTime();
new Date().valueOf();
new Date()*1
验证:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<button>点击</button>
<script>
/*
* 连续点击只会1000执行一次btnClick函数
*/
let obutton = document.getElementsByTagName('button')[0]
// 如果用箭头函数,箭头函数没有arguments,也不能通过apply改变this指向
function btnClick() {
console.log('我响应了')
}
/*
方法1: 定时器方式实现
缺点:第一次触发事件不会立即执行fn,需要等delay间隔过后才会执行
*/
let throttle = (fn, delay) => {
let flag = false
return function(...args) {
if (flag) return
flag = true
setTimeout(() => {
fn(...args)
flag = false
}, delay)
}
}
/*
方法2:时间戳方式实现
缺点:最后一次触发回调与前一次的触发回调的时间差小于delay,则最后一次触发事件不会执行回调
*/
// fn 是需要执行的函数
// wait 是时间间隔
const throttle2 = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
obutton.onclick = throttle(btnClick, 1000)
</script>
</body>
</html>