首頁 > web前端 > js教程 > 從混亂到清晰:JavaScript 中函數組合和管道的聲明式方法

從混亂到清晰:JavaScript 中函數組合和管道的聲明式方法

Patricia Arquette
發布: 2025-01-08 18:40:41
原創
231 人瀏覽過

From Chaos to Clarity: A Declarative Approach to Function Composition and Pipelines in JavaScript

目錄

  • 整潔程式碼的藝術
  • 純函數的魔力
  • 用函數組合搭建橋樑
  • 使用管道簡化程式碼
  • 調整管道以滿足不斷變化的需求
  • 避免函數組合的陷阱
  • 邁向優雅的旅程

乾淨程式碼的藝術?

你是否曾經盯著別人的程式碼思考,「這是什麼樣的魔法?」你沒有解決真正的問題,而是迷失在循環、條件和變數的迷宮中。這是所有開發者面臨的鬥爭-混亂與清晰之間的永恆之戰。

程式碼應該編寫供人類閱讀,並且只是順便供機器執行。 — Harold Abelson

但是不要害怕! 乾淨的程式碼並不是隱藏在開發者地牢中的神秘寶藏——它是一項你可以掌握的技能。其核心在於聲明式編程,其中焦點轉移到代碼做什麼,而將如何留在後台。

讓我們透過一個例子來實現這一點。假設您需要找到清單中的所有偶數。以下是我們中的許多人以命令式方法開始的:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evenNumbers.push(numbers[i]);
  }
}
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

當然,它有效。但說實話,它很吵:手動循環、索引追蹤和不必要的狀態管理。乍一看,很難看出程式碼到底在做什麼。現在,讓我們將其與聲明式方法進行比較:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

一行,沒有雜亂-只有明確的意圖:「過濾偶數。」這是簡單和重點與複雜和噪音之間的區別。

為什麼清潔代碼很重要? ‍?

乾淨的程式碼不僅僅是為了看起來漂亮,而是為了更聰明地工作。六個月後,您願意在令人困惑的邏輯迷宮中掙扎,還是閱讀實際上可以自我解釋的程式碼?

雖然命令式程式碼佔有一席之地,尤其是在效能至關重要的情況下,但聲明性程式碼通常以其可讀性和易於維護性而獲勝。

這是一個快速並排比較:

Imperative Declarative
Lots of boilerplate Clean and focused
Step-by-step instructions Expresses intent clearly
Harder to refactor or extend Easier to adjust and maintain

一旦你接受了乾淨的聲明式程式碼,你就會想知道如果沒有它你是如何管理的。這是建立可預測、可維護系統的關鍵——而這一切都始於純函數的魔力。因此,拿起你的編碼棒(或一杯濃咖啡☕),加入更乾淨、更強大的程式碼的旅程。 ?✨


純函數的魔力?

您是否遇到過一個函數試圖執行所有操作 - 獲取數據、處理輸入、記錄輸出,甚至可能沖泡咖啡?這些多任務野獸可能看起來高效,但它們是被詛咒的文物:脆弱、複雜,維護起來是一場噩夢。當然,一定有更好的方法。

簡單是可靠性的先決條件。 — Edsger W. Dijkstra

純淨的本質⚗️

純函數就像施放一個完美的咒語——對於相同的輸入它總是產生相同的結果,沒有副作用。這種魔法簡化了測試、簡化了調試並抽象化了複雜性以確保可重用性。

要看差異,這裡有一個不純函數:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evenNumbers.push(numbers[i]);
  }
}
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

這個函數會修改全域狀態-就像一個出錯的咒語一樣,它不可靠且令人沮喪。它的輸出依賴於不斷變化的折扣變量,將偵錯和重複使用變成了一項乏味的挑戰。

現在,讓我們來做一個純函數:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

沒有全域狀態,這個函數是可預測的且是獨立的。測試變得簡單,並且可以作為更大工作流程的一部分進行重複使用或擴展。

透過將任務分解為小的、純函數,您可以建立一個既健全又令人愉快的程式碼庫。所以,下次你寫函數時,問問自己:「這個咒語是否專注且可靠——或者它會成為一個被詛咒的神器,準備釋放混亂嗎?」


用功能組合搭建橋樑?

有了純函數,我們就掌握了簡單的技巧。就像樂高積木? ,它們是獨立的,但僅靠積木並不能建造一座城堡。神奇之處在於將它們結合起來——函數組合的本質,其中工作流程在抽象實現細節的同時解決問題。

讓我們透過一個簡單的例子來看看它是如何運作的:計算購物車的總數。首先,我們將可重複使用的實用函數定義為建構塊:

let discount = 0;   

const applyDiscount = (price: number) => {
  discount += 1; // Modifies a global variable! ?
  return price - discount;
};

// Repeated calls yield inconsistent results, even with same input!
console.log(applyDiscount(100)); // Output: 99
console.log(applyDiscount(100)); // Output: 98
discount = 100;
console.log(applyDiscount(100)); // Output: -1 ?
登入後複製
登入後複製
登入後複製

現在,我們將這些實用函數組合成一個工作流程:

const applyDiscount = (price: number, discountRate: number) => 
  price * (1 - discountRate);

// Always consistent for the same inputs
console.log(applyDiscount(100, 0.1)); // 90
console.log(applyDiscount(100, 0.1)); // 90
登入後複製
登入後複製
登入後複製

這裡,每個函數都有明確的目的:求和價格、應用折扣以及對結果進行四捨五入。它們一起形成一個邏輯流,其中一個的輸出饋入下一個。 域邏輯很清楚-計算有折扣的結帳總額。

此工作流程體現了函數組合的力量:專注於內容(程式碼背後的意圖),同時讓如何(實作細節)淡入背景。


使用管道簡化程式碼✨

函數組合很強大,但隨著工作流程的增長,深度嵌套的組合可能會變得難以遵循,就像拆包俄羅斯娃娃? 。管道進一步抽象,提供反映自然推理的線性轉換序列。

建立一個簡單的管道實用程式? ️

許多 JavaScript 庫(你好,函數式編程愛好者!?)都提供管道實用程序,但創建自己的庫卻出奇地簡單:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evenNumbers.push(numbers[i]);
  }
}
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

此實用程式將操作連結成清晰、漸進的流程。使用管道重構我們先前的結帳範例可以得到:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

結果幾乎是詩意的:每個階段都建立在上一個階段的基礎上。這種一致性不僅美觀,而且實用,使工作流程足夠直觀,即使是非開發人員也可以追蹤並理解正在發生的事情。

與 TypeScript 的完美合作?

TypeScript 透過定義嚴格的輸入輸出關係來確保管道中的類型安全。使用函數重載,您可以輸入以下管道實用程式:

let discount = 0;   

const applyDiscount = (price: number) => {
  discount += 1; // Modifies a global variable! ?
  return price - discount;
};

// Repeated calls yield inconsistent results, even with same input!
console.log(applyDiscount(100)); // Output: 99
console.log(applyDiscount(100)); // Output: 98
discount = 100;
console.log(applyDiscount(100)); // Output: -1 ?
登入後複製
登入後複製
登入後複製

未來一瞥?

雖然創建自己的實用程式很有洞察力,但 JavaScript 提議的管道運算符 (|>) 將使使用本機語法的連結轉換變得更加簡單。

const applyDiscount = (price: number, discountRate: number) => 
  price * (1 - discountRate);

// Always consistent for the same inputs
console.log(applyDiscount(100, 0.1)); // 90
console.log(applyDiscount(100, 0.1)); // 90
登入後複製
登入後複製
登入後複製

管道不僅簡化了工作流程,還減少了認知開銷,提供了超出程式碼範圍的清晰度和簡單性。


調整管道以滿足不斷變化的需求?

在軟體開發中,需求可能會瞬間改變。管道使適應變得毫不費力——無論您是添加新功能、重新排序流程還是完善邏輯。讓我們透過一些實際場景來探討管道如何處理不斷變化的需求。

新增稅計算? ️

假設我們需要在結帳過程中包含銷售稅。管道使這一切變得簡單 - 只需定義新步驟並將其插入正確的位置即可:

type CartItem = { price: number };

const roundToTwoDecimals = (value: number) =>
  Math.round(value * 100) / 100;

const calculateTotal = (cart: CartItem[]) =>
  cart.reduce((total, item) => total + item.price, 0);

const applyDiscount = (discountRate: number) => 
  (total: number) => total * (1 - discountRate);
登入後複製
登入後複製

如果要求發生變化(例如在折扣之前徵收銷售稅),管道可以輕鬆適應:

// Domain-specific logic derived from reusable utility functions
const applyStandardDiscount = applyDiscount(0.2);

const checkout = (cart: CartItem[]) =>
  roundToTwoDecimals(
    applyStandardDiscount(
      calculateTotal(cart)
    )
  );

const cart: CartItem[] = [
  { price: 19.99 },
  { price: 45.5 },
  { price: 3.49 },
];

console.log(checkout(cart)); // Output: 55.18
登入後複製
登入後複製

新增條件功能:會員折扣? ️

管道還可以輕鬆處理條件邏輯。想像一下為會員提供額外折扣。首先,定義一個實用程式來有條件地應用轉換:

const pipe =
  (...fns: Function[]) =>
  (input: any) => fns.reduce((acc, fn) => fn(acc), input);
登入後複製
登入後複製

接下來,將其動態合併到管道中:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evenNumbers.push(numbers[i]);
  }
}
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

恆等函數充當空操作,使其可重用於其他條件轉換。這種靈活性使管道能夠無縫適應不同的條件,而不會增加工作流程的複雜性。

擴展調試管道?

調試管道可能會讓人感覺很棘手——就像大海撈針一樣——除非您配備了正確的工具。一個簡單但有效的技巧是插入日誌函數來闡明每個步驟:

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // Output: [2, 4]
登入後複製
登入後複製
登入後複製
登入後複製

雖然管道和函數組合提供了顯著的靈活性,但了解它們的怪癖可以確保您能夠運用它們的力量,而不會陷入常見的陷阱。


避免函數組合的陷阱? ️

函數組合和管道為您的程式碼帶來了清晰和優雅,但就像任何強大的魔法一樣,它們也可能隱藏著陷阱。讓我們揭開它們並學習如何輕鬆避免它們。

陷阱#1:意想不到的副作用?

副作用可能會潛入您的作品中,將可預測的工作流程變成混亂的工作流程。修改共享狀態或依賴外部變數可能會使您的程式碼變得不可預測。

let discount = 0;   

const applyDiscount = (price: number) => {
  discount += 1; // Modifies a global variable! ?
  return price - discount;
};

// Repeated calls yield inconsistent results, even with same input!
console.log(applyDiscount(100)); // Output: 99
console.log(applyDiscount(100)); // Output: 98
discount = 100;
console.log(applyDiscount(100)); // Output: -1 ?
登入後複製
登入後複製
登入後複製

修正:確保管道中的所有函數都是純淨的。

const applyDiscount = (price: number, discountRate: number) => 
  price * (1 - discountRate);

// Always consistent for the same inputs
console.log(applyDiscount(100, 0.1)); // 90
console.log(applyDiscount(100, 0.1)); // 90
登入後複製
登入後複製
登入後複製

陷阱#2:管道過於複雜?

管道非常適合分解複雜的工作流程,但過度使用可能會導致難以遵循的混亂鏈條。

type CartItem = { price: number };

const roundToTwoDecimals = (value: number) =>
  Math.round(value * 100) / 100;

const calculateTotal = (cart: CartItem[]) =>
  cart.reduce((total, item) => total + item.price, 0);

const applyDiscount = (discountRate: number) => 
  (total: number) => total * (1 - discountRate);
登入後複製
登入後複製

修正:將相關步驟分組到封裝意圖的高階函數。

// Domain-specific logic derived from reusable utility functions
const applyStandardDiscount = applyDiscount(0.2);

const checkout = (cart: CartItem[]) =>
  roundToTwoDecimals(
    applyStandardDiscount(
      calculateTotal(cart)
    )
  );

const cart: CartItem[] = [
  { price: 19.99 },
  { price: 45.5 },
  { price: 3.49 },
];

console.log(checkout(cart)); // Output: 55.18
登入後複製
登入後複製

陷阱#3:調試盲點?

調試管道時,確定哪個步驟導致了問題可能具有挑戰性,尤其是在長鏈中。

修正:注入日誌記錄或監視函數來追蹤中間狀態,正如我們之前在每個步驟中列印訊息和值的日誌函數所看到的那樣。

陷阱#4:類別方法中的上下文遺失?

當從類別中編寫方法時,您可能會丟失正確執行它們所需的上下文。

const pipe =
  (...fns: Function[]) =>
  (input: any) => fns.reduce((acc, fn) => fn(acc), input);
登入後複製
登入後複製

修正:使用 .bind(this) 或箭頭函數來保留上下文。

const checkout = pipe(
  calculateTotal,
  applyStandardDiscount,
  roundToTwoDecimals
);
登入後複製

透過留意這些陷阱並遵循最佳實踐,無論您的需求如何變化,您都將確保您的組合和管道保持高效且優雅。


走向優雅的旅程?

掌握函數組合和管道不僅僅是為了編寫更好的程式碼,而是為了發展你的思維方式,超越實現。它是關於打造能夠解決問題的系統,讀起來像一個講得很好的故事,並透過抽象和直覺的設計激發靈感。

無需重新發​​明輪子?

像 RxJS、Ramda 和 lodash-fp 這樣的函式庫提供了由活躍社群支援的生產就緒、經過實戰測試的實用程式。它們使您能夠專注於解決特定領域的問題,而不是擔心實現細節。

指導你練習的要點? ️

  • 乾淨的程式碼:乾淨的程式碼不僅僅是外觀,而是更聰明地工作。它創建的解決方案可簡化調試、協作和維護。六個月後,您會感謝自己編寫的程式碼實際上能夠自我解釋。
  • 函數組合:結合純粹、專注的函數來設計工作流程,優雅、清晰地解決複雜問題。
  • 管道:透過將邏輯塑造成清晰、直觀的流程來抽象化複雜性。如果做得好,管道可以提高開發人員的工作效率並使工作流程變得如此清晰,即使是非開發人員也可以理解它們。

最終,您的程式碼不僅僅是一系列指令——它是您正在講述的故事,您正在施展的咒語。精心打造,讓優雅引領您的旅程。 ?✨

以上是從混亂到清晰:JavaScript 中函數組合和管道的聲明式方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
作者最新文章
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板