0


【前端精进之路】JS篇:第13期 函数式编程

文章目录

写在前面

这里是小飞侠Pan🥳,立志成为一名优秀的前端程序媛!!!

本篇文章收录于我的专栏:前端精进之路

同时收录于我的github前端笔记仓库中,持续更新中,欢迎star~

👉https://github.com/mengqiuleo/myNote


一、什么是函数式编程

介绍

函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,主要是利用函数把运算过程封装起来,通过组合各种函数来计算结果。函数式编程意味着你可以在更短的时间内编写具有更少错误的代码。

常见特性

无副作用

指调用函数时不会修改外部状态,即一个函数调用 n 次后依然返回同样的结果。

var a =1;// 含有副作用,它修改了外部变量 a// 多次调用结果不一样functiontest1(){
  a++return a;}// 无副作用,没有修改外部状态// 多次调用结果一样functiontest2(a){return a +1;}

透明引用

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

var a =1;var b =2;// 函数内部使用的变量并不属于它的作用域functiontest1(){return a + b;}// 函数内部使用的变量是显式传递进去的functiontest2(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 })

纯函数

如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入的参数(相同的输入,必须得到相同的输出)。

/ 纯函数
constcalculatePrice=(price,discount)=> price * discount
let price = calculatePrice(200,0,8)
console.log(price)// 不纯函数constcalculatePrice=(price,discount)=>{const dt=newDate().toISOString()
        console.log(`${dt}:${something}`)return something
 }foo('hello')

函数副作用

  • 当调用函数时,除了返回函数值外,还对注调用函数产生附加的影响
  • 例如修改全局变量(函数外的变量)或修改参数//函数外a被改变,这就是函数的副作用let a =5letfoo=()=>(a = a *10)foo()console.log(a)// 50let 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}letfoo=data=>{
  data.count =3}
console.log(data.count)// 1foo(data)
console.log(data.count)// 3// 改进后使我们的数据具有不可变性let data ={count:1}letfoo=data=>{let lily =JSON.parse(JSON.stringify(data))// let lily= {...data} 使用扩展运算符去做拷贝,只能拷贝第一层
  lily.count =3}
console.log(data.count)// 1foo(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

// 普通函数functionadd(x,y){return x + y;}add(1,2);// 3// 函数柯里化varadd=function(x){returnfunction(y){return x + y;};};var increment =add(1);increment(2);// 3

作用:

主要有 3 个作用: 参数复用提前返回延迟执行

参数复用:我们可以传入部分参数,然后拿着返回的函数再加入新的参数,这样前面传入的参数就做到了参数复用。

提前返回 和 延迟执行 也很好理解,因为每次调用函数时,它只接受一部分参数,并返回一个函数(提前返回),直到(延迟执行)传递所有参数为止。

实现

functioncurrying(fn,...args){const length = fn.length;let allArgs =[...args];constres=(...newArgs)=>{
    allArgs =[...allArgs,...newArgs];if(allArgs.length === length){returnfn(...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 函数,它可以接受多个函数作为参数,然后返回一个新的函数。当我们为这个新函数传递参数时,该参数就会「流」过其中的函数,最后返回结果。

// 用法如下:functionfn1(x){return x +1;}functionfn2(x){return x +2;}functionfn3(x){return x +3;}functionfn4(x){return x +4;}const a =compose(fn1, fn2, fn3, fn4);
console.log(a(1));// 1+4+3+2+1=11

我们compose的作用就是将嵌套执行的方法作为参数平铺,嵌套执行的时候,里面的方法就是右边的方法最开始执行,然后往左边返回。

functioncompose(...fns){return(arg)=> 
    fns.reduceRight((prev,fn)=>{returnfn(prev)},arg)}

解释

首先我们要传入一些函数,即

...fns

。并且通过compose函数我们要

return

一个函数。

return的这个函数传入的参数就是我们要计算的初始值。

注意:如果箭头函数的返回值只有一个式子,那么可以不使用{}和return。

在函数体的操作中,我们需要从右往左遍历所有的函数,prev是上一次返回的值,fn是当前处理的函数,并且要把arg当做初始值传入。

在函数体中,我们要返回当前fn函数的执行结果作为prev的值。

pipe函数

compose

执行是从右到左的。而管道函数,执行顺序是从左到右执行的

functioncompose(...fns){return(arg)=> 
    fns.reduce((prev,fn)=>{returnfn(prev)},arg)}

五、偏函数

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

  • 柯里化:f(a,b,c)=f(a)(b)©
  • 偏函数:f(a,b,c)=f(a,b)©
/*
    用bind函数实现偏函数,bind的另一个用法使一个函数拥有预设的初始参数,将这些参数写在bind的第一个参数后,
    当绑定函数调用时,会插入目标函数的初始位置,调用函数传入的参数会跟在bind传入的后面
  */letadd=(x, y)=> x + y
let rst =add.bind(null,1)rst(2)//3

六、防抖与节流

防抖

你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

functiondebounce(fn,delay){//1.定义一个定时器,保存上一次的定时器let timer =null//2.真正执行的函数returnfunction(...args){//取消上一次的定时器if(timer){clearTimeout(timer)}//延迟执行
        timer =setTimeout(()=>{//执行外部传入的函数fn.apply(this,args)}, delay);}}
  • this绑定:真正执行的函数是要绑定我们返回的函数,而箭头函数没有this,它的this指向上层作用域,正好是我们返回的函数
  • 参数传递:因为真正执行的函数是我们返回的函数,所以参数是放在返回的那个函数中,直接解构

验证

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><inputtype="text"><script>functiondebounce(fn,delay){//1.定义一个定时器,保存上一次的定时器let timer =null//2.真正执行的函数returnfunction(...args){//取消上一次的定时器if(timer){clearTimeout(timer)}//延迟执行
        timer =setTimeout(()=>{//执行外部传入的函数fn.apply(this,args)}, delay);}}const inputEl = document.querySelector('input');let counter =0constinputChange=function(){
      console.log(`发送了第${++counter}次网络请求`)}

    inputEl.oninput =debounce(inputChange,1000)</script></body></html>

节流

当事件持续触发时,节流操作可以稀释事件处理函数执行频率,假设在

1s

onmousemove

事件触发了

100

次,通过节流就可以使得

onmousemove

事件的事件处理函数每

100ms

触发一次,也就是在

1s

onmousemove

事件的事件处理函数只执行

10

次。

方法1: 定时器方式实现

缺点:第一次触发事件不会立即执行fn,需要等delay间隔过后才会执行

letthrottle=(fn, delay)=>{let flag =falsereturnfunction(...args){if(flag)return
        flag =truesetTimeout(()=>{fn(...args)
          flag =false}, delay)}}

方法2:时间戳方式实现

缺点:最后一次触发回调与前一次的触发回调的时间差小于delay,则最后一次触发事件不会执行回调

constthrottle2=(fn, wait =50)=>{// 上一次执行 fn 的时间let previous =0// 将 throttle 处理结果当作函数返回returnfunction(...args){// 获取当前时间,转换成时间戳,单位毫秒let now =+newDate()// 将当前时间和上一次执行函数的时间进行对比// 大于等待时间就把 previous 设置为当前时间并执行函数 fnif(now - previous > wait){
            previous = now
            fn.apply(this, args)}}}

**解释这里的

+new Date()

**

js在某个数据类型前使用‘+’,这个操作目的是为了将该数据类型转换为Number类型,如果转换失败,则返回NaN;

+'2'+1// 3+[1]// NaN

+new Date() 会调用Date.prototype 上面的 valueOf方法

下面的例子返回效果等同:

+newDate();newDate().getTime();newDate().valueOf();newDate()*1

验证

<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"/><metaname="viewport"content="width=device-width, initial-scale=1.0"/><metahttp-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指向functionbtnClick(){
      console.log('我响应了')}/*
            方法1: 定时器方式实现
            缺点:第一次触发事件不会立即执行fn,需要等delay间隔过后才会执行
         */letthrottle=(fn, delay)=>{let flag =falsereturnfunction(...args){if(flag)return
        flag =truesetTimeout(()=>{fn(...args)
          flag =false}, delay)}}/*
            方法2:时间戳方式实现
            缺点:最后一次触发回调与前一次的触发回调的时间差小于delay,则最后一次触发事件不会执行回调
          */// fn 是需要执行的函数// wait 是时间间隔constthrottle2=(fn, wait =50)=>{// 上一次执行 fn 的时间let previous =0// 将 throttle 处理结果当作函数返回returnfunction(...args){// 获取当前时间,转换成时间戳,单位毫秒let now =+newDate()// 将当前时间和上一次执行函数的时间进行对比// 大于等待时间就把 previous 设置为当前时间并执行函数 fnif(now - previous > wait){
          previous = now
          fn.apply(this, args)}}}

    obutton.onclick =throttle(btnClick,1000)</script></body></html>

本文转载自: https://blog.csdn.net/weixin_52834435/article/details/125775222
版权归原作者 小飞侠Pan 所有, 如有侵权,请联系我们删除。

“【前端精进之路】JS篇:第13期 函数式编程”的评论:

还没有评论