JS之函数式编程

252 阅读4分钟

1. 什么是函数式编程

函数式编程是一种编程范式,就是将运算过程封装成一个函数,通过组合各种函数来计算结果。讲到编程范式,现在主要有两种:命令式编程和声明式编程。

命令式编程是一种描述电脑所需作出的行为的编程范式,是目前使用最广泛的,其本质就是站在计算机的角度思考问题,关注计算机执行步骤,通过编写一条一条的指令让计算机执行一些动作。

声明式编程描述的是目标的性质,让计算机明白目标而不是具体的实现过程。通过定义具体的规则,以便系统底层可以自动实现具体功能。

声明式函数编程可以让我们专注于怎么实现代码,而不用考虑函数内部是如何实现的,降低了代码的复杂性。

2. 函数式编程的特点

2.1 函数是一等公民

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

2.2 只用表达式,不用语句

函数式编程就是为了处理运算,而表达式意味着运算过程需要有返回值,如果使用语句就是执行某项操作,没有返回值。

2.3 没有副作用

副作用指的就是函数内部与外部互动,产生运算以外的其他结果。函数式编程调用函数时不会修改外部状态,也就是一个函数调用n次后依然返回相同的结果。

2.4 引用透明

指的是函数的运行不依赖外部的的变量,只会用到传递给它的变量以及自己内部创建的变量。

2.5 不可变变量

一个变量一旦创建以后,就不能再修改,任何修改都会创建一个新的变量。

3.闭包

3.1 概念

闭包是指有权访问另一个函数(通常是外部函数)作用域中变量的函数(注意:闭包是在使用时才会生成的,而非创建时)。

function hd() {
    let n = 1
    return function sum() {
        console.log(n++)
    }
}
let a = hd()
a() //1
a() //2
a() //3

解析:函数只有在调用的时候才会创建一块新的内存,hd()调用后创建一个父环境,里面包含变量n和sum函数,每次调用a()都会创建一个新的子环境,且都能访问到外部的变量n。

  • 优点: (1)保护:闭包可以保护函数的私有变量不受外部干扰,形成不被销毁的栈内存;
    (2)保存:闭包可以将上级作用域的引用保存下来,实现方法/属性的私有化。
  • 缺点: 若不清除闭包,闭包会一直占用内容,造成内存泄漏

3.2 setTimeout问题

for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log(i)
    },1000)
}
console.log(i)
结果:
立即打出3
随后隔1s打出33

setTimeout是异步函数,所以先会执行主线程的console.log(i),此时for循环已经循环完毕,i=3退出循环,然后在从任务队列中调出setTimeout执行(有3个),但此时i=3,所以1s后会输出3个3

但是若想打印出3 -> 0,1,2,怎么实现?

for (var i = 0; i < 3; i++) {
    (function (j) {
        setTimeout(function () {
            console.log(j)
        }, 1000)
    })(i)
}
console.log(i)
结果: 3 -> 0,1,2

利用立即执行函数,每次调用执行立即函数就会创建出一块新的内存,保证每次循环的i被保存下来

3.3 使用闭包获取商品区间

let arr = [1,3,6,8,9,24,7,45,32,67]

let res = arr.filter(function (price) {
    return price > 10 && price < 50
})
let res1 = arr.filter(function (price) {
    return price > 3 && price < 20
})

console.log(res)  //[ 24, 45, 32 ]
console.log(res1)  //[ 6, 8, 9, 7 ]

使用filter函数对数组进行筛选,但是这样每次修改区间都要写重复代码,因此可以将返回值部分封装成一个函数,提高代码复用。

function between(a,b) {
    return function (v) {
        return v > a && v < b
    }
}

let res2 = arr.filter(between(10,50))
console.log(res2) //[ 24, 45, 32 ]

3.4 移动动画闭包的使用

需求:点击按钮,按钮每100ms向右移动1

<style>
    button {
        position: absolute;
    }
</style>

<body>
    <button>点击1</button>
    <button>点击2</button>
</body>

版本一:

<script>
    let btns = document.querySelectorAll('button')
    btns.forEach(function (btn) {
        btn.addEventListener('click', function () {
            let left = 1
            setInterval(function () {
                btn.style.left = (left++) + 'px'
            }, 100)
        })
    })
</script>

问题:但是这样的话连续点击按钮就会出现抖动,原因是每次点击,都会创建一个新环境,这个环境中包含变量left = 1 和 setInterval(),所以每次点击的left都为1,就会出现抖动情况。
解决:将let left = 1放到上一级环境中,闭包可以保存上级引用,这样就避免了left值每次都为1
版本二:

<script>
    let btns = document.querySelectorAll('button')
    btns.forEach(function (btn) {
        let left = 1 //挪到这里了~
        btn.addEventListener('click', function () {
            setInterval(function () {
                btn.style.left = (left++) + 'px'
            }, 100)
        })
    })
</script>

问题:此时动画还有一个问题,就是频繁点击会加速的问题。每点击一次就会增加一个定时器,所以时间间隔就会< 100ms
解决:不让每次点击产生这么多定时器就好了
版本三:

<script>
    let btns = document.querySelectorAll('button')
    btns.forEach(function (btn) {
        let left = 1
        let flag = false   //设置一个flag
        btn.addEventListener('click', function () {
            if (!flag) {
                flag = true  //当点击后,将flag变为true,下次点击就不会到后面的代码了
                setInterval(function () {
                    btn.style.left = (left++) + 'px'
                }, 100)
            }
        })

    })
</script>

3.5 闭包内存泄露的解决方法

<body>
    <div desc = 'food'>汉堡包</div>
    <div desc = 'drink'>可乐</div>
</body>
<script>
    let divs = document.querySelectorAll('div')
    divs.forEach(function (item) {
        item.addEventListener('click', function () {
            console.log(item.getAttribute('desc'))
            console.log(item)
        })
    })
</script>

输出:
food
<div desc='food'>汉堡包</div>

这样每次点击还会将上一级item打印出来,但实际我们只需要获得desc的值就可以,并不需要item

<script>
    let divs = document.querySelectorAll('div')
    divs.forEach(function (item) {
        let desc = item.getAttribute('desc')
        item.addEventListener('click', function () {
            console.log(desc)
        })
        item = null //将item赋值为空,清除内存占用
    })
</script>