你不知道的JS专栏 - 避免bug利器纯函数

你不知道的JS专栏 - 避免bug利器纯函数

目录:

  • 纯函数的概念及基本认识

  • 纯函数在实际开发中的使用案例

  • 纯函数在框架中的使用, 以及框架中的纯函数思想

纯函数的概念及基本认识

纯函数定义 - 不依赖除参数外的任何其他外部作用域变量, 同时也不修改其作用域外任何变量的函数

纯函数的作用 - 由于纯函数定义的特质, 所以纯函数不需要考虑任何上下文环境, 也不会被上下文环境所影响, 只需要考虑函数接收了什么参数, 需要返回什么值, 从而导致bug发生概率大大降低

这个定义可能不太理解, 我们来直接看个例子, 看看他有什么问题

let nameArr = ['loki']
function addName(arr) {
    arr.push('thor');
}
addName(nameArr);
console.log(nameArr); // ['loki', 'thor']

我们会发现, 我们在进行名字添加操作的时候, nameArr中确实是出现了一个新的数组项thor, 很多朋友可能会说, 这代码没问题啊, 我不就是想给他加个名字进去吗, 我实现了啊, 确实你功能的确实现了, 但是这个代码也响应的有了一些小瑕疵

你无法回退到执行addName之前的nameArr状态

由于你对原数组nameArr进行了修改, 如果你的代码其他地方有用到nameArr且nameArr值必须为[‘loki’]的时候, 你的代码实际上就已经出现了危机, 而出现这种危机的契机就是你写的这个addName不够纯净

// 我们将addName改为纯函数
let nameArr = ['loki']
function addName(arr) {
    let newArr = [...arr];
    return newArr;
}
let newNameArr = addName(nameArr);
console.log(newNameArr); // ['loki', 'thor']
console.log(nameArr); // ['loki']

这样如果你的代码在其他地方用到了nameArr, 也不会被影响到, 而你也可以用newNameArr代替nameArr做你要做的事情, 比如渲染进页面又或者进行循环操作

重点

所谓的不依赖外部变量和不修改外部变量本质上其实就是你写的这个函数在执行时, 任何外部作用域变量的变化都不能更改函数执行的轨迹 称之为不依赖(参数除外), 不修改则是任何函数执行中的操作都不会对外部作用域的任何变量进行修改, 我们在开发中总是会遇到一个变量被几个方法来回调用和修改,这个时候如果你没有纯函数的思想的话, 很可能会方法之间互相影响, 最终导致某个方法因为拿不到自己想要的变量值从而行为异常

纯函数在实际开发中的使用案例

我们要做一个功能, 页面中展示了几种水果, 我们点击 删除第一个水果按钮, 第一个水果就删除, 点击重置按钮, 所有的水果又都回来, 这个功能很简单, 如下图, 而我相信大部分的小白程序员会写出图片之后代码块中的代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umr4gp9g-1583811990916)('...')]

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>纯函数</title>
</head>

<body>
    <div class="container">
        <button class="deleteFst">删除排在第一个的水果</button>
        <button class="resetAll">还原所有的水果</button>
        <div class="list"></div>
    </div>
    <script>
        (function () {
            // 假设这个likeArr是从后端请求过来的数据, 
            // 且这个likeArr最终会被渲染进页面的list中
            const likeArr = ['Apple', 'Banana', 'Orange'];
            const deleteFst = document.querySelector('.deleteFst'); // 删除按钮
            const resetAll = document.querySelector('.resetAll'); // 重置按钮
            const list = document.querySelector('.list'); // 容器
            
            // 初始化函数
            function init() {
                render(likeArr);
                bindEvent();
            }
            
            function bindEvent() {
                // 点击清空按钮的时候, 执行删除函数
                // 点击重置按钮的时候, 执行重置函数
                deleteFst.addEventListener('click', () => {deleteFstFunc(likeArr)}, false);
                resetAll.addEventListener('click', () => resetAllFunc, false);
            }

            function render(arr) {
                list.innerHTML = '';
                let domArr = arr.map(fruit => {
                    let div = document.createElement('div');
                    div.innerText = fruit;
                    return div;
                })
                console.log(domArr);
                domArr.forEach(dom => list.appendChild(dom));
            }

            // 当我们点击删除第一个水果的时候, 页面的第一个水果会被删除
            function deleteFstFunc(arr) {
                arr.shift();
                render(arr);
            }

            // 当我们点击重置所有水果的时候, 页面的水果回复
            function resetAllFunc() {
                render(likeArr); // 将likeArr重新渲染
            }
            init();    
        }())
    </script>
</body>

</html>

实现效果如下, 我们发现删除确实没有任何问题, 但是我们的重置却无法重置, 但是也没有报错,实际上是因为我们在delete第一个水果的时候直接修改了likeArr, 导致likeArr一直被清空, 而之后我们再去重置的时候likeArr已经是一个空数组了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QIDWrjMF-1583811990917)('...')]

上面这份代码看似没有什么问题, 但是实际效果却并不会如人所愿, 出现了一些bug, 在我们的日常开发中, 也会经常遇到这种逻辑上其实看起来没什么问题, 却又出现了bug的情况, 这也是我们写代码的方式不够高级, 所以导致我们有些隐形的bug没办法规避, 他不会通过报错来提醒你, 因为逻辑上和语法上你都没有错误, 跑的通, 而解决这些问题的根本方式就是从一开始就写一些更加稳固的代码来避免这类不会报错的bug发生, 而纯函数就是这些高阶代码中的一名成员, 他可以帮助我们很好的规避上面的bug

// 于是我们将刚刚的js代码进行修改
 (function () {
    const likeArr = ['Apple', 'Banana', 'Orange'];
    const deleteFst = document.querySelector('.deleteFst');
    const resetAll = document.querySelector('.resetAll'); 
    const list = document.querySelector('.list'); 
    let controllArr = likeArr; // 增加了这一行

    function init() {
        render(likeArr);
        bindEvent();
    }
    
    function bindEvent() {
        deleteFst.addEventListener('click', () => {
            // 更改了这个函数
            let lastArr = deleteFstFunc(controllArr);
            controllArr = lastArr;
            render(lastArr);
        }, false);
        resetAll.addEventListener('click', () => {
            controllArr = likeArr; // 增加了这句
            resetAllFunc();
        }, false);
    }

    function render(arr) {
        list.innerHTML = '';
        let domArr = arr.map(fruit => {
            let div = document.createElement('div');
            div.innerText = fruit;
            return div;
        })
        console.log(domArr);
        domArr.forEach(dom => list.appendChild(dom));
    }

    function deleteFstFunc(arr) {
        // 纯函数deleteFstFunc
        let newArr = [...arr];
        newArr.shift();
        return newArr;
    }

    function resetAllFunc() {
        render(likeArr); 
    }
    init();    
}())

笔者修改了deleteFstFunc和增加controllerArr并对点击事件进行一定更新以后, 效果实现, 而你真正需要注意的其实是deleteFstFunc中的思想

纯函数在框架中的使用, 以及框架中的纯函数思想

如果你使用过React, 那么setState我相信你不会陌生, 如果你没用过, 那么小程序中的setState你应该也有印象, 如果你都没接触过, 那就来看看混个眼熟, 我只是想写写在框架中某些地方的注意点, 它并不影响你吸收纯函数的思想(如今React在函数组件中加入了HOOK, 所以未来useState可能用的不多, 但是主要是为了将某些问题凸显出来)

我们现在有一个需求, 当用户查看学生时, 用户可以进行搜索, 而当用户输入完毕以后, 我们需要对应的将列表展示给用户, 不输入则默认展示所有学生, 效果如下

![react实例效果]

// 为了方便观看, 笔者将会把所有代码写在一个student.js中, 将不会进行组件的拆分 
import React from 'react';

export default class Student extends React.PureComponent {
    componentDidMount() {
        // 假设进行数据请求, 请求过来的值是arr
        const arr = ['loki', 'andy', 'thor', 'kate']
        this.setState({
            studentArr: arr
        })
    }

    state = {
        studentArr: []
    }

    render() {
        let domArr = this.state.studentArr.map((stu, index) => <div key={`${stu}_${index}`}>{ stu }</div>)
        return (
            <div>
                <input onInput={this.changeList} type="text"/>
                { domArr }
            </div>
        );
    }

    changeList = (e) => {
        let value = e.target.value;
        if(!value) {
            this.setState({
                studentArr: [...this.state.studentArr]      
            })
        }
        let filterArr = this.state.studentArr.filter(stu => stu.includes(value));
        console.log(filterArr);
        this.setState({
            studentArr: filterArr
        })
    }
}

当我们进行搜索的时候, 确实可以检索某一部分值出来, 但是当我们清空搜索框, 却发现不能回到所有值的状态,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ypvkJZo0-1583811990918)('..')]

主要原因我想大家应该是很清楚的, 因为我们直接更改了state中studentArr的值, 函数不纯净, 从而导致其他依赖该studentArr的操作行为异常, 解决的方法也很简单, 我们将changeList函数变成一个纯函数

import React from 'react';

export default class Student extends React.PureComponent {
    componentDidMount() {
        // 假设进行数据请求, 请求过来的值是arr
        const arr = ['loki', 'andy', 'thor', 'kate']
        this.setState({
            studentArr: arr
        })
    }

    state = {
        studentArr: [],
        keyWords: '' // 建立一个keyWords索引
    }

    render() {
        let newArr = this.changeList(this.state.keyWords); // 渲染的最后是newArr
        let domArr = newArr.map((stu, index) => <div key={`${stu}_${index}`}>{ stu }</div>)
        return (
            <div>
                <input onInput={(e) => {
                    this.setState({
                        keyWords: e.target.value
                    })
                }} type="text"/>
                { domArr }
            </div>
        );
    }

    // 更改了changeList的函数体, 使其成为一个纯函数
    changeList = (value) => {
        let lastFilterArr = [];
        if(!value) lastFilterArr = [...this.state.studentArr];
        else lastFilterArr = this.state.studentArr.filter(stu => stu.includes(value));
        return lastFilterArr;
    }
}

自此效果实现

上方的代码不会对整个studentArr进行任何的影响, 因此依赖于studentArr的操作也不会行为异常了

小提示

在redux中, 也运用了纯函数的思想进行处理, 比如reducer, 但是涉及到了源码部分, 所以有兴趣的朋友可以自己去查看

小提示

我们并非一定在任何时候都要求写出纯函数, 这样反而会影响我们的开发, 笔者建议, 如果要对需要反复更新渲染的数据进行更改的时候, 建议使用纯函数进行更改, 就像上方的deleteFstFunc一样, 也可以看出笔者其实在其他地方并没有使用到纯函数, 也大方的使用了全局变量来方便自己的开发, 所以就跟设计模式一样, 不要近乎疯狂的去追求他, 而是记住他的思想和理念, 在自己的日常开发中, 如果遇到真真正正你觉得改变原数据可能会带来危险的地方, 我希望你可以想到纯函数, 纯函数也迎合设计模式的思想

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值