前端開發(fā)中使用純函數(shù)提純非純函數(shù)

理解純函數(shù)和非純函數(shù)是向更清晰、更基于角色和可測試的代碼的簡單過渡。在這篇文章中,將通過查看一個簡單的體重指數(shù)(BMI)計算器來探索純函數(shù)和非純函數(shù),該計算器通過一些簡單的身高和體重輸入因素來估算“健康體重”。

概念

上文簡單介紹了什么是純函數(shù)和非純函數(shù),這里在簡單列舉一下:

  • 純函數(shù):純函數(shù)更容易理解,特別是因為代碼庫可以擴展,以及基于角色的函數(shù)可以完成一項工作并且做得很好。純函數(shù)不會修改作用域之外的外部變量、狀態(tài)、數(shù)據(jù),并且在給定相同輸入的情況下返回相同的輸出,這樣的函數(shù)就被認為是“純”的。

  • 非純函數(shù):在其作用域范圍之外改變變量、狀態(tài)、數(shù)據(jù)的函數(shù),因此因此將其視為“不純”。編寫 JavaScript 的方法有很多種,從非純/純函數(shù)的角度考慮,可以編寫更容易推理的代碼。

下面以完全不純的方式創(chuàng)建的 BMI 計算器,然后再將其重構(gòu)為多個使用純函數(shù)的函數(shù)。

HTML和事件

這是創(chuàng)建的用于捕獲用戶輸入數(shù)據(jù)的表單:

<form name="bmi">
    <h1>BMI計算器</h1>
    <label>
        <input type="text" name="weight" placeholder="體重 (kg)" />
    </label>
    <label>
        <input type="text" name="height" placeholder="身高 (cm)" />
    </label>
    <button type="submit">立即計算</button>
    <div class="calculation">
        <div>BMI值: <span class="result"></span></div>
        <div>健康指數(shù):<span class="health"></span></div>
    </div>
</form>

下面為按鈕增加事件監(jiān)聽器:

<script>
    (() => {
        const elForm = document.querySelector("form[name=bmi]");
        const onSubmit = (event) => {
            event.preventDefault();
        };

        elForm.addEventListener("submit", onSubmit, false);
    })();
</script>

非純的實現(xiàn)

現(xiàn)在將刪除 IIFE 和事件處理程序的內(nèi)容,并專注于 onSubmit 函數(shù):

const onSubmit = (event) => {
    event.preventDefault();

    let healthMessage;
    const result = elForm.querySelector(".result");
    const health = elForm.querySelector(".health");
    const weight = parseInt(
        elForm.querySelector("input[name=weight]").value,
        10
    );
    const height = parseInt(
        elForm.querySelector("input[name=height]").value,
        10
    );
    const bmi = (weight / (((height / 100) * height) / 100)).toFixed(1);
    if (bmi < 18.5) {
        healthMessage = "偏瘦體重";
    } else if (bmi > 18.5 && bmi < 25) {
        healthMessage = "健康體重";
    } else if (bmi > 25) {
        healthMessage = "肥胖體重";
    }

    result.innerHTML = bmi;
    health.innerHTML = healthMessage;
};

這就是 onSubmit 事件函數(shù)包含的所有內(nèi)容,輸入身高/體重,它就會用這些結(jié)果更新 DOM。現(xiàn)在,這就是非純函數(shù)極難調(diào)試和理解的地方,特別是對于非前端開發(fā)人員來。當然為了代碼的易讀性,可以將其加些注釋,如下:

const onSubmit = (event) => {
    // 阻止表單實際提交
    event.preventDefault();

    let healthMessage;
    // 獲取DOM result 和 health 的 <span> 標簽以將結(jié)果注入
    const result = elForm.querySelector(".result");
    const health = elForm.querySelector(".health");

    // 根據(jù)重量和身高值解析為基數(shù)為 10 的整數(shù)
    const weight = parseInt(
        elForm.querySelector("input[name=weight]").value,
        10
    );
    const height = parseInt(
        elForm.querySelector("input[name=height]").value,
        10
    );
    const bmi = (weight / (((height / 100) * height) / 100)).toFixed(1);
    if (bmi < 18.5) {
        healthMessage = "偏瘦體重";
    } else if (bmi > 18.5 && bmi < 25) {
        healthMessage = "健康體重";
    } else if (bmi > 25) {
        healthMessage = "肥胖體重";
    }
    // 將結(jié)果插入到相應(yīng)的 DOM
    result.innerHTML = bmi;
    health.innerHTML = healthMessage;
};

看上去好像容易理解,然后需要大量的注釋,很多時候都無法如何去描述。下面就以純函數(shù)的方式對其重構(gòu)。

純函數(shù)的實現(xiàn)

在開始使用純函數(shù)之前,在上面不純的實現(xiàn)中,onSubmit 函數(shù)中做了太多的事情:

  • 從 DOM 中讀取值
  • 將值解析為數(shù)字
  • 從解析值計算BMI
  • 有條件地檢查 BMI 結(jié)果并將正確的消息分配給未定義的變量 healthMessage
  • 將值寫入 DOM

為了提純實現(xiàn),將要實現(xiàn)處理這些操作的函數(shù):

  • 將值解析為數(shù)字并計算 BMI
  • 將綁定到 DOM 的正確消息返回

開始提純

先從輸入值解析計算BMI開始,專門針對這一段代碼:

const weight = parseInt(elForm.querySelector("input[name=weight]").value, 10);
const height = parseInt(elForm.querySelector("input[name=height]").value, 10);

const bmi = (weight / (((height / 100) * height) / 100)).toFixed(1);

這涉及 parseInt() 計算BMI的公式,當在應(yīng)用程序中的某個時刻重構(gòu)或添加更多功能時,這不是很靈活并且很可能很容易出錯(客戶端輸入是不可靠的)。

為了重構(gòu),只需要單獨獲取每個輸入的 value 屬性,并將它們委托給一個 getBMI 函數(shù):

const weight = elForm.querySelector("input[name=weight]").value;
const height = elForm.querySelector("input[name=height]").value;

const bmi = getBMI(weight, height);

這個 getBMI 函數(shù)將是 100% 純的,因為它接受參數(shù)并根據(jù)這些參數(shù)返回一個新的數(shù)據(jù),給定相同的輸入,將獲得相同的輸出。具體代碼如下:

const getBMI = (weight, height) => {
    const newWeight = parseInt(weight, 10);
    const newHeight = parseInt(height, 10);
    return (newWeight / (((newHeight / 100) * newHeight) / 100)).toFixed(1);
};

此函數(shù)將 weightheight 作為參數(shù),將它們轉(zhuǎn)換為數(shù)字 parseInt ,然后執(zhí)行 BMI 的計算。無論是將 String 還是 Number 作為每個參數(shù)傳遞,都可以安全檢查。

進入下一個函數(shù)。healthMessage 的計算邏輯,如下:

health.innerHTML = getHealthMessage(bmi);

同樣,開始實現(xiàn) getHealthMessage 的邏輯:

const getHealthMessage = (bmiValue) => {
    let healthMessage;
    if (bmiValue < 18.5) {
        healthMessage = "偏瘦體重";
    } else if (bmiValue > 18.5 && bmiValue < 25) {
        healthMessage = "健康體重";
    } else if (bmiValue > 25) {
        healthMessage = "肥胖體重";
    }
    return healthMessage;
};

上面的函數(shù)也是 100% 的純函數(shù),輸入輸出固定。

至此,代碼就完成了重構(gòu),完整代碼如下:

<script>
    (() => {
        const elForm = document.querySelector("form[name=bmi]");

        const getHealthMessage = (bmiValue) => {
            let healthMessage;
            if (bmiValue < 18.5) {
                healthMessage = "偏瘦體重";
            } else if (bmiValue > 18.5 && bmiValue < 25) {
                healthMessage = "健康體重";
            } else if (bmiValue > 25) {
                healthMessage = "肥胖體重";
            }
            return healthMessage;
        };

        const getBMI = (weight, height) => {
            let newWeight = parseInt(weight, 10);
            let newHeight = parseInt(height, 10);
            return (
                newWeight /
                (((newHeight / 100) * newHeight) / 100)
            ).toFixed(1);
        };

        const onSubmit = (event) => {
            event.preventDefault();

            const result = elForm.querySelector(".result");
            const health = elForm.querySelector(".health");

            const weight = elForm.querySelector("input[name=weight]").value;
            const height = elForm.querySelector("input[name=height]").value;

            const bmi = getBMI(weight, height);

            result.innerHTML = bmi;
            health.innerHTML = getHealthMessage(bmi);
        };

        elForm.addEventListener("submit", onSubmit, false);
    })();
</script>

在前端項目開發(fā)中很難保證所有函數(shù)都是純函數(shù),因為只要需要和 DOM 進行交互的函數(shù)就不是純函數(shù),因此在代碼優(yōu)化過程中,不要過份追求純函數(shù),盡可能的提純函數(shù)即可。