你不知道的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一樣, 也可以看出筆者其實在其他地方並沒有使用到純函數, 也大方的使用了全局變量來方便自己的開發, 所以就跟設計模式一樣, 不要近乎瘋狂的去追求他, 而是記住他的思想和理念, 在自己的日常開發中, 如果遇到真真正正你覺得改變原數據可能會帶來危險的地方, 我希望你可以想到純函數, 純函數也迎合設計模式的思想

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章