// 每日前端夜話 第396篇
// 正文共:2600 字
// 預計閱讀時間:8 分鐘
介紹先簡要介紹一下面向對象和函數式編程。
兩者都是編程範式,在允許和禁止的技術上有所不同。
有僅支持一種範式的程式語言,例如 Haskell(純函數式)。
還有支持多種範式的語言,例如 JavaScript,你可以用 JavaScript 編寫面向對象的代碼或函數式代碼,甚至可以將兩者混合。
創建項目在深入探究這兩種編程範式之間的差異之前,先創建一個階乘計算器項目。
首先創建所需的所有文件和文件夾,如下所示:
$ mkdir func-vs-oop
$ cd ./func-vs-oop
$ cat index.html
$ cat functional.js
$ cat oop.js接下來在 index.html 內創建一個簡單的表單。
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">
<script src="functional.js" defer></script>
</head>
<body>
<div class="container mt-5">
<div class="container mt-3 mb-5 text-center">
<h2>Functional vs OOP</h2>
</div>
<form id="factorial-form">
<div class="form-group">
<label for="factorial">Factorial</label>
<input class="form-control" type="number" name="factorial" id="factorial" />
</div>
<button type="submit" class="btn btn-primary">Calculate</button>
</form>
<div class="container mt-3">
<div class="row mt-4 text-center">
<h3>Result:</h3>
<h3 class="ml-5" id="factorial-result"></h3>
</div>
</div>
</div>
</body>
</html>為了使界面看上去不那麼醜陋,我們把 bootstrap 作為 CSS 框架。
如果在瀏覽器中顯示這個 HTML,應該是這樣的:
現在這個表單還沒有任何操作。
我們的目標是實現一種邏輯,在該邏輯中你可以輸入一個最大為 100 的數字。單擊「Calculate」按鈕後,結果應顯示在 result-div 中。
下面分別以面向對象和函數式的方式來實現。
函數式實現首先為函數式編程方法創建一個文件。
$ cat functional.js首先,需要一個在將此文件加載到瀏覽器時要調用的函數。
該函數先獲取表單,然後把我們需要的函數添加到表單的提交事件中。
function addSubmitHandler(tag, handler) {
const form = getElement(tag);
form.addEventListener('submit', handler);
}
addSubmitHandler("#factorial-form", factorialHandler);首先聲明一個名為 addSubmitHandler 的函數。
這個函數有兩個參數,第一個是要在 HTML 中查找的標籤,第二個是要綁定到 Element 的 commit-event 的函數。
接下來,通過傳入#factorial-form 和函數名 factorialHandler 來調用此函數。
標籤前面的 # 表明我們正在尋找 HTML 中的 id 屬性。
如果現在嘗試運行該代碼,則會拋出錯誤,因為在任何地方都還沒有定義函數 getElement 和 factorialHandler。
因此,首先在 addSubmitHandler 函數前面定義 getElement ,如下所示:
function getElement(tag) {
return document.querySelector(tag);
}這個函數非常簡單,只返回通過傳入的標記找到的 HTML元素。但是稍後我們將重用此功能。
現在添加 factorialHandler 函數來創建核心邏輯。
function factorialHandler(event) {
event.preventDefault();
const inputNumber = getValueFromElement('#factorial');
try {
const result = calculateFactorial(inputNumber);
displayResult(result);
} catch (error) {
alert(error.message);
}
}把事件傳回後立即調用 preventDefault 。
這將阻止 Submit 事件的默認行為,你可以試試不調用 preventDefault 時單擊按鈕後會發生什麼。
之後,通過調用 getValueFromElement 函數從輸入欄位中獲取用戶輸入的值。在得到數字後,用函數 calculateFactorial 計算階乘,然後通過將結果傳遞給函數 displayResult 將結果展示到頁面。
如果該值的格式不正確或者數字大於 100,將會拋出錯誤並彈出 alert。
下一步,創建另外兩個輔助函數:getValueFromElement 和 displayResult,並將它們添加到 getElement 函數後面。
function getValueFromElement(tag) {
return getElement(tag).value;
}
function displayResult(result) {
getElement('#factorial-result').innerHTML = result
}這兩個函數都使用我們的 getElement 函數。這種可重用性是為什麼函數式編程如此有效的一個原因。
為了使它更加可重用,可以在 displayResult 上添加名為 tag 第二個參數。
這樣就可以動態設置應該顯示結果的元素。
但是在本例中,我用了硬編碼的方式。
接下來,在 factoryHandler 前面創建 calculateFactorial 函數。
function calculateFactorial(number) {
if (validate(number, REQUIRED) && validate(number, MAX_LENGTH, 100) && validate(number, IS_TYPE, 'number')) {
return factorial(number);
} else {
throw new Error(
'Invalid input - either the number is to big or it is not a number'
);
}
}接著創建一個名為 validate 的函數來驗證參數 number 是否為空且不大於 100,且類型為 number。如果檢查通過,就調用 factorial 函數並返回其結果。如果沒有通過,則拋出在 factorialHandler 函數中捕獲的錯誤。
const MAX_LENGTH = 'MAX_LENGTH';
const IS_TYPE = 'IS_TYPE';
const REQUIRED = 'REQUIRED';
function validate(value, flag, compareValue) {
switch (flag) {
case REQUIRED:
return value.trim().length > 0;
case MAX_LENGTH:
return value <= compareValue;
case IS_TYPE:
if (compareValue === 'number') {
return !isNaN(value);
} else if (compareValue === 'string') {
return isNaN(value);
}
default:
break;
}
}在這個函數中,用 switch 來確定要執行的驗證範式類型。這只是一個簡單的值驗證。
然後在 calculateFactorial 聲明的前面添加實際的 factor 函數。這是最後一個函數。
function factorial(number) {
let returnValue = 1;
for (let i = 2; i <= number; i++) {
returnValue = returnValue * i;
}
return returnValue;
}最終的 functional.js 文件下所示:
const MAX_LENGTH = 'MAX_LENGTH';
const IS_TYPE = 'IS_TYPE';
const REQUIRED = 'REQUIRED';
function getElement(tag) {
return document.querySelector(tag);
}
function getValueFromElement(tag) {
return getElement(tag).value;
}
function displayResult(result) {
getElement('#factorial-result').innerHTML = result
}
function validate(value, flag, compareValue) {
switch (flag) {
case REQUIRED:
return value.trim().length > 0;
case MAX_LENGTH:
return value <= compareValue;
case IS_TYPE:
if (compareValue === 'number') {
return !isNaN(value);
} else if (compareValue === 'string') {
return isNaN(value);
}
default:
break;
}
}
function factorial(number) {
let returnValue = 1;
for (let i = 2; i <= number; i++) {
returnValue = returnValue * i;
}
return returnValue;
}
function calculateFactorial(number) {
if (validate(number, REQUIRED) && validate(number, MAX_LENGTH, 100) && validate(number, IS_TYPE, 'number')) {
return factorial(number);
} else {
throw new Error(
'Invalid input - either the number is to big or it is not a number'
);
}
}
function factorialHandler(event) {
event.preventDefault();
const inputNumber = getValueFromElement('#factorial');
try {
const result = calculateFactorial(inputNumber);
displayResult(result);
} catch (error) {
alert(error.message);
}
}
function addSubmitHandler(tag, handler) {
const form = getElement(tag);
form.addEventListener('submit', handler);
}
addSubmitHandler("#factorial-form", factorialHandler);在這種方法中,我們專門處理函數。每個函數都只有一個目的,大多數函數可以在程序的其他部分中重用。
對於這個簡單的 Web 程序,使用函數式的方法有些過分了。接著將編寫相同的功能,只不過這次是面向對象的。
面向對象的實現首先,需要將 index.html 文件的腳本標籤中的 src 更改為以下內容。
<script src="oop.js" defer></script>然後創建 oop.js 文件。
$ cat oop.js對於面向對象方法,我們要創建三種不同的類,一種用於驗證,一種用於階乘計算,另一種用於處理表單。
先是創建處理表單的類。
class InputForm {
constructor() {
this.form = document.getElementById('factorial-form');
this.numberInput = document.getElementById('factorial');
this.form.addEventListener('submit', this.factorialHandler.bind(this));
}
factorialHandler(event) {
event.preventDefault();
const number = this.numberInput.value;
if (!Validator.validate(number, Validator.REQUIRED)
|| !Validator.validate(number, Validator.MAX_LENGTH, 100)
|| !Validator.validate(number, Validator.IS_TYPE, 'number'))
{
alert('Invalid input - either the number is to big or it is not a number');
return;
}
const factorial = new Factorial(number);
factorial.display();
}
}
new InputForm();在構造函數中獲取 form-element 和 input-element 並將其存儲在類變量(也稱為屬性)中。之後將方法 factorialHandler 添加到 Submit-event 中。在這種情況下需要把類的 this 綁定到方法。如果不這樣做,將會得到一個引用錯誤,例如調用 this.numberInput.value 將會是 undefined。之後以事件為參數創建類方法 factorialHandler。
該方法的代碼看起來應該有點熟悉,例如 if 語句檢查輸入值是否有效,就像在 calculateFactorial 函數中所做的那樣。Validator.validate 是對我們仍然需要創建的 Validator 類中的靜態方法的調用。如果使用靜態方法,則無需初始化對象的新實例。驗證通過後創建 Factorial 類的新實例,傳遞輸入值,然後將計算的結果顯示給用戶。
接下來在 InputForm 類 前面創建 Validator 類。
class Validator {
static MAX_LENGTH = 'MAX_LENGTH';
static IS_TYPE = 'IS_TYPE';
static REQUIRED = 'REQUIRED';
static validate(value, flag, compareValue) {
switch (flag) {
case this.REQUIRED:
return value.trim().length > 0;
case this.MAX_LENGTH:
return value <= compareValue;
case this.IS_TYPE:
if (compareValue === 'number') {
return !isNaN(value);
} else if (compareValue === 'string') {
return isNaN(value);
}
default:
break;
}
}
}這個類內部的所有內容都是靜態的,所以我們不需要任何構造函數。
這樣做的好處是不需要在每次使用它時都初始化該類。
validate 與 validate 函數與我們的 functional.js 幾乎完全相同。
接下來在 Validator 類的後面創建 Factorial 類。
class Factorial {
constructor(number) {
this.resultElement = document.getElementById('factorial-result');
this.number = number;
this.factorial = this.calculate();
}
calculate() {
let returnValue = 1;
for (let i = 2; i <= this.number; i++) {
returnValue = returnValue * i;
}
return returnValue;
}
display() {
this.resultElement.innerHTML = this.factorial;
}
}在初始化這個類的實例後,我們獲得 resultElement 並將其存儲為屬性以及我們傳入的數字。
之後調用方法 calculate 並將其返回值存儲在屬性中。calculate 方法包含與 functional.js 中的 factor 函數相同的代碼。最後是 display 方法,該方法將結果元素的 innerHTML 設置為現實計算出的階乘數。
完整的 oop.js 文件如下所示。
class Validator {
static MAX_LENGTH = 'MAX_LENGTH';
static IS_TYPE = 'IS_TYPE';
static REQUIRED = 'REQUIRED';
static validate(value, flag, compareValue) {
switch (flag) {
case this.REQUIRED:
return value.trim().length > 0;
case this.MAX_LENGTH:
return value <= compareValue;
case this.IS_TYPE:
if (compareValue === 'number') {
return !isNaN(value);
} else if (compareValue === 'string') {
return isNaN(value);
}
default:
break;
}
}
}
class Factorial {
constructor(number) {
this.resultElement = document.getElementById('factorial-result');
this.number = number;
this.factorial = this.calculate();
}
calculate() {
let returnValue = 1;
for (let i = 2; i <= this.number; i++) {
returnValue = returnValue * i;
}
return returnValue;
}
display() {
this.resultElement.innerHTML = this.factorial;
}
}
class InputForm {
constructor() {
this.form = document.getElementById('factorial-form');
this.numberInput = document.getElementById('factorial');
this.form.addEventListener('submit', this.factorialHandler.bind(this));
}
factorialHandler(event) {
event.preventDefault();
const number = this.numberInput.value;
if (!Validator.validate(number, Validator.REQUIRED)
|| !Validator.validate(number, Validator.MAX_LENGTH, 100)
|| !Validator.validate(number, Validator.IS_TYPE, 'number'))
{
alert('Invalid input - either the number is to big or it is not a number');
return;
}
const factorial = new Factorial(number);
factorial.display();
}
}
new InputForm();我們創建了三個類來處理程序的三個不同的功能:
總結兩種方法都是編寫代碼的有效方法。我喜歡在自己不同項目中嘗試最有效的方法。在很多情況下,甚至不可能如此清晰地分離這兩種範式。
希望這篇文章可以使你對不同的編程方法有一個基本的了解。