JS代碼髒亂差?你須要知道這些優化技巧

JS代碼髒亂差?你須要知道這些優化技巧


image.png




做者 | Ilya Suzdalnitski譯者 | 王強編輯 | Yoniehtml

JavaScript 是萬衆矚目的力量。它是世界上最流行的編程語言。它容易理解,有豐富的學習資源,對初學者很是友好。JavaScript 有着龐大的資源庫,對小公司和大企業都頗具吸引力。龐大的 JS 工具和庫生態系統爲開發者的生產力帶來了福音。只用 JS 一種語言就能統一前端和後端,因而就能夠在整個技術棧中使用同一套技能組合。前端

JavaScript 的力量就像核能

JavaScript 提供了許多工具和選項,但它對開發者幾乎沒有任何限制。讓沒有經驗的人使用 JavaScript,就像是給一個兩歲的孩子一盒火柴和一罐汽油同樣......java

JavaScript 的力量就像核能——既能夠用來爲城市供電,也能夠用來摧毀一切。用 JavaScript 構建東西很容易。但構建既可靠又易維護的軟件就不是什麼輕鬆的事情了。ios

代碼可靠性

在建造大壩時,工程師首先關注的是可靠性。在沒有任何規劃或安全措施的前提下修建大壩是很危險的。建造橋樑、生產飛機、汽車...... 都是一回事。若是汽車不安全不可靠,那麼像馬力、引擎響聲,仍是內飾中使用的皮革類型這些都可有可無了。git

一樣,每位軟件開發者的目標都是編寫可靠的軟件。若是代碼有缺陷且不可靠,那麼其餘問題都是小巫見大巫了。編寫可靠代碼的最佳方法是什麼?那就是寫出簡潔的代碼。簡單的反面是複雜。所以軟件開發者首要的責任應該是下降代碼的複雜度程序員

開發者經驗豐富的標誌就是能編寫可靠的軟件。可靠性還包括可維護性——只有可維護的代碼庫纔是可靠的。es6

雖然我是函數式編程的堅決信徒,但我不會安利什麼內容。我只是會從函數式編程中引用一些概念,並演示如何在 JavaScript 中應用它們。github

咱們真的須要軟件可靠性嗎?這取決於你本身。有些人認爲客戶能湊合用軟件就好了,我不敢苟同。事實上,若是軟件不可靠且難以維護,那麼其餘問題根本就不重要了。誰會購買一輛隨機剎車和加速的汽車呢?誰會但願本身的手機天天斷線幾回,隨機重啓呢?面試

內存不足

咱們怎樣開發可靠的軟件?數據庫

首先考慮可用內存的大小。咱們的程序應該儘可能節約內存,永遠不會耗盡全部可用內存,以免性能降低。

這和編寫可靠的軟件有什麼關係?人類的大腦也有本身的內存,叫作工做記憶。咱們的大腦是宇宙中已知最強大的機器,但它有本身的一套限制——咱們只能在工做記憶中保存大約五條信息。

對於編程工做來講,這意味着簡單的代碼消耗的腦力資源更少,進而提高咱們的效率,併產出更可靠的軟件。本文和一些 JavaScript 工具將幫助你實現這一目標!

初學者的注意事項

本文中我將大量使用 ES6 函數。簡單回顧一下:

// ---------------------------------------------
// lambda (fat arrow) anonymous functions
// ---------------------------------------------

const doStuff = (a, b, c) => {...}

// same as:
function doStuff(a, b, c) {
 ...
}


// ---------------------------------------------
// object destructuring
// ---------------------------------------------

const doStuff = ({a, b, c}) => {
 console.log(a);
}

// same as:
const doStuff = (params) => {
 const {a, b, c} = params;

 console.log(a);
}

// same as:
const doStuff = (params) => {
 console.log(params.a);
}


// ---------------------------------------------
// array destructuring
// ---------------------------------------------

const [a, b] = [1, 2];

// same as:
const array = [1, 2];
const a = array[0];
const b = array[1];
    工具   

JavaScript 的最大優點之一是豐富的可用工具。沒有其餘哪一種編程語言有如此龐大的工具和庫生態系統。

咱們應該充分利用這些工具,尤爲是 ESLint(https://eslint.org/)。ESLint 是靜態代碼分析工具,能夠找到代碼庫中潛在的問題,維持代碼庫的高質量。並且 linting 是一個徹底自動化的過程,能夠防止低質量代碼進入代碼庫。

不少人沒能充分利用 ESLint——他們只用了預建配置,如 eslint-config-airbnb 而已。很惋惜這只是 ESlint 的皮毛。JavaScript 是一種沒有限制的語言。而 linting 設置不當會帶來深遠的影響。

熟練的開發者不只知道該用哪些函數,還會知道不該該使用哪些 JS 函數。JavaScript 是一種古老的語言,有不少包袱,因此區分好壞是很重要的。

ESLint 配置 你能夠按以下方式設置 ESLint。 我建議逐一熟悉這些建議,並將 ESLint 規則逐一歸入你的項目中。 先將它們配置爲 warn,習慣了能夠將一些規則轉爲 error。

在項目的根目錄中運行:

npm i -D eslint
npm i -D eslint-plugin-fp

而後在項目的根目錄中建立一個.eslintrc.yml 文件:

env:
 es6: true

plugins:
 fp

rules:
 # rules will go in here

若是你使用的是像 VSCode 這樣的 IDE,請安裝 ESLint 插件。

你還能夠從命令行手動運行 ESLint:

npx eslint .
重構的重要性

重構是下降現有代碼複雜度的過程。若是使用得當,它將成爲咱們對付可怕的技術債務怪物的最佳武器。若是沒有持續的重構,技術債務將不斷積累,反過來又會拖累開發者。

重構就是清理現有代碼,同時確保代碼仍能正常運行的過程。重構是軟件開發中的良好實踐,是健康組織中開發流程的一部分。

須要注意的是,在重構以前最好將代碼歸入自動化測試。重構時很容易在無心中破壞現有功能,全面的測試套件是預防潛在風險的好辦法。

複雜度的最大源頭

這可能聽起來很奇怪,但代碼自己就是複雜度的最大源頭。實際上NoCode就是編寫安全可靠軟件的最佳途徑。但不少時候咱們作不到NoCode,因此備選答案就是減小代碼量。更少的代碼意味着更少的複雜度,也意味着產生錯誤的潛在區域更少。有人說初級開發者編寫代碼,而高級開發者刪除代碼——不能贊成更多。

長文件

人類是懶惰的。懶惰是一種短時間生存策略,捨棄對生存不重要的事物來節省能量。

有些人很懶,不守規矩。人們將愈來愈多的代碼放入同一個文件中...... 若是文件的長度沒有限制,那麼這些文件每每會無限增加下去。根據個人經驗,超過 200 行代碼的文件就太難理解、太難維護了。長文件還意味着程序可能處理的工做太多了,違反了單一責任原則。

怎麼解決這個問題?只需將大文件分解爲更細粒度的模塊便可。

建議的 ESLint 配置:

rules:
 max-lines:
 - warn
 - 200
長函數

複雜度的另外一大來源是漫長而複雜的函數,很難推理;並且函數的職責太多,很難測試。

例以下面的 express.js 代碼片斷是用來更新博客條目的:

router.put('/api/blog/posts/:id', (req, res) => {
 if (!req.body.title) {
   return res.status(400).json({
     error: 'title is required',
   });
 }

 if (!req.body.text) {
   return res.status(400).json({
     error: 'text is required',
   });
 }

 const postId = parseInt(req.params.id);

 let blogPost;
 let postIndex;
 blogPosts.forEach((post, i) => {
   if (post.id === postId) {
     blogPost = post;
     postIndex = i;
   }
 });

 if (!blogPost) {
   return res.status(404).json({
     error: 'post not found',
   });
 }

 const updatedBlogPost = {
   id: postId,
   title: req.body.title,
   text: req.body.text
 };

 blogPosts.splice(postIndex, 1, updatedBlogPost);

 return res.json({
   updatedBlogPost,
 });
});

函數體長度爲 38 行,執行如下操做:分析 post id、查找現有博客帖子、驗證用戶輸入、在輸入無效的狀況下返回驗證錯誤、更新帖子集合,並返回更新的博客帖子。

顯然它能夠重構爲一些較小的函數。路由處理程序可能看起來像這樣:

router.put("/api/blog/posts/:id", (req, res) => {
 const { error: validationError } = validateInput(req.body);
 if (validationError) return errorResponse(res, validationError, 400);

 const { blogPost } = findBlogPost(blogPosts, req.params.id);

 const { error: postError } = validateBlogPost(blogPost);
 if (postError) return errorResponse(res, postError, 404);

 const updatedBlogPost = buildUpdatedBlogPost(req.body);

 updateBlogPosts(blogPosts, updatedBlogPost);

 return res.json({updatedBlogPost});
});

推薦的 ESLint 配置:

rules:
 max-lines-per-function:
 - warn
 - 20
複雜函數

複雜函數每每就是長函數,反之亦然。函數之因此變複雜可能有不少因素,但其中嵌套回調和圈複雜度較高都是比較容易解決的。

嵌套回調每每致使回調地獄。能夠用 promise 處理回調,而後使用 async-await 就能削弱其影響。

來看一個帶有深度嵌套回調的函數:

fs.readdir(source, function (err, files) {
 if (err) {
   console.error('Error finding files: ' + err)
 } else {
   files.forEach(function (filename, fileIndex) {
     gm(source + filename).size(function (err, values) {
       if (err) {
         console.error('Error identifying file size: ' + err)
       } else {
         aspect = (values.width / values.height)
         widths.forEach(function (width, widthIndex) {
           height = Math.round(width / aspect)
           this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
             if (err) console.error('Error writing file: ' + err)
           })
         }.bind(this))
       }
     })
   })
 }
})
 圈複雜度

函數複雜度的另外一大來源是圈複雜度。它指的是給定函數中的語句(邏輯)數,諸如 if 語句、循環和 switch 語句。這些函數很難推理,要儘可能避免使用。這是一個例子:

if (conditionA) {
 if (conditionB) {
   while (conditionC) {
     if (conditionD && conditionE || conditionF) {
       ...
     }
   }
 }
}

推薦的 ESLint 配置:

rules:
 complexity:
 - warn
 - 5

 max-nested-callbacks:
 - warn
 - 2
 max-depth:
 - warn
 - 3

另外一個下降代碼複雜度的方法是聲明式代碼,稍後會具體展開。

可變狀態

狀態是存儲在內存中的臨時數據,例如對象中的變量或字面量。狀態自己是無害的,但可變狀態是軟件複雜度的最大源頭之一,與面向對象結合時尤爲如此(稍後將詳細介紹)。

 人腦的侷限

如前所述,人類大腦是宇宙中已知最強大的機器。然而咱們的大腦很難應付狀態,由於咱們在工做記憶中一次只能容納五件事情。咱們很容易推理一段代碼自己的做用,但涉及到它對代碼庫中變量的影響時就會糊塗了。

使用可變狀態編程容易讓人精神錯亂。只要放棄可變狀態,咱們的代碼就能變得更加可靠。

 可變狀態的問題

舉個例子:

const increasePrice = (item, increaseBy) => {
 // never ever do this
 item.price += increaseBy;

 return item;
};

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// prints newItem.price 13
console.log('newItem.price', newItem.price);

// prints oldItem.price 13
// unexpected?
console.log('oldItem.price', oldItem.price);

錯誤很難看出來:咱們改變函數參數時不當心修改了原始項目的價格。原本應該是 10,實際上改爲了 13。

咱們構造和返回一個不可變的新對象來解決這個問題:

const increasePrice = (item, increaseBy) => ({
 ...item,
 price: item.price + increaseBy
});

const oldItem = { price: 10 };

const newItem = increasePrice(oldItem, 3);

// prints newItem.price 13
console.log('newItem.price', newItem.price);

// prints oldItem.price 10
// as expected!
console.log('oldItem.price', oldItem.price);

請記住,使用 ES6 spread 等運算符複製時會生成淺拷貝,而不是深拷貝——它不會複製任何嵌套屬性。若是上面的 item 具備 item.seller.id 這樣的內容,則新 item 的 seller 仍將引用舊 item,這是不行的。在 JavaScript 中使用不可變狀態時,一些較爲穩健的方法包括 immutable.js 和 Ramda lense 等。我將在另外一篇文章中介紹這些選項。

建議的 ESLint 配置:

rules:
 fp/no-mutation: warn
 no-param-reassign: warn
不要 push 數組

在數組突變中使用像 push 這樣的方法也存在一樣的問題:

const a = ['apple', 'orange'];
const b = a;

a.push('microsoft')

// ['apple', 'orange', 'microsoft']
console.log(a);

// ['apple', 'orange', 'microsoft']
// unexpected?
console.log(b);

數組 b 本應保持不變的。咱們建立一個新數組而不是調用 push 就能夠了。

構造新數組來避免問題:

const newArray = [...a, 'microsoft'];
 不肯定性

不肯定性是說程序在輸入不變的狀況下輸出卻沒法肯定。明明 2 + 2 == 4,但不肯定性程序不必定得出這個結果。

雖然可變狀態自己並非不肯定性的,但它會使代碼更容易出現不肯定性(如上所示)。諷刺的是最流行的編程範式(OOP 和命令式編程)特別容易產生不肯定性。

不變性

想要避免可變性的缺陷,最好的方法就是改用不變性。不變性一個很大的話題,我可能會專門撰文討論它。

建議的 ESLint 配置:

rules:
 fp/no-mutating-assign: warn
 fp/no-mutating-methods: warn
 fp/no-mutation: warn
 避免使用 Let 關鍵字

咱們不該該用 var 在 JavaScript 中聲明變量,一樣咱們也應該避免使用 let 關鍵字。用 let 聲明的變量能夠被從新分配,讓代碼更難推理。本質上這也是人腦工做記憶的一種限制。使用 let 關鍵字編程時,咱們必須記住全部反作用和潛在的極端狀況。咱們可能不當心爲變量分配一個不正確的值,結果就得浪費時間來調試了。

單元測試受其影響最嚴重。多數測試都是並行開展的,因此在多個測試之間共享可變狀態是一種災難。

let 關鍵字的替代方案固然是 const 關鍵字。雖然它不能保證不變性,但它會禁止從新分配,使代碼更易推理。大多數狀況下,從新給變量賦值的代碼能夠被提取到一個單獨的函數中。來看一個例子:

let discount;

if (isLoggedIn) {
 if (cartTotal > 100  && !isFriday) {
   discount = 30;
 } else if (!isValuedCustomer) {
   discount = 20;
 } else {
   discount = 10;
 }
} else {
 discount = 0;
}

將同一個示例提取到一個函數中:

const getDiscount = ({isLoggedIn, cartTotal, isValuedCustomer}) => {
 if (!isLoggedIn) {
   return 0;
 }

 if (cartTotal > 100  && !isFriday()) {
   return 30;
 }

 if (!isValuedCustomer) {
   return 20;
 }

 return 10;
}

一開始不用 let 可能會不習慣,但這樣代碼會更簡潔易懂。我好久沒用 let 了,一點都不想它。

養成不使用 let 關鍵字編程的習慣可讓你更有條理。你必須將代碼分解爲更小,更易於管理的函數組合,進而讓函數的職責更清晰,更好地分離關注點,並使代碼庫更具可讀性和可維護性。

建議的 ESLint 配置:

rules:
 fp/no-let: warn
面向對象編程

Java 是自 MS-DOS 以來計算行業最使人痛苦的事情。」

—— 名 Alan Kay,面向對象編程的發明者

面向對象編程是一種用來組織代碼的流行編程範例。本節會討論 Java、C#、JavaScript、TypeScript 等語言中使用的主流 OOP 的侷限。我不會批判正確的 OOP(例如 SmallTalk)。

若是你認爲在開發軟件時必須使用 OOP,則能夠跳過本節。

 優秀程序員和普通程序員

優秀的程序員會編寫好的代碼,普通的程序員編寫錯誤的代碼,不管什麼編程範式都是如此。編程範式要作的是防止普通的程序員搞出太多破壞。無論你願不肯意,你都會和普通的程序員共事。惋惜 OOP 沒有足夠的約束力來防止他們形成巨大的傷害。

OOP 的初衷是幫助程序員打理代碼庫。諷刺的是人們認爲 OOP 能夠下降複雜度,但它提供的工具彷佛只是在增長複雜度而已。

 OOP 不肯定性

OOP 代碼容易出現不肯定性——它嚴重依賴可變狀態,不像函數式編程那樣能夠保證輸出不變,讓代碼更難推理。涉及併發時這種問題更爲嚴重。

 共享可變狀態

「我以爲用可變對象構建大型對象圖會讓面向對象的大型程序愈來愈複雜。你得試着理解並記住你在調用一種方法時會發生什麼,反作用會是什麼。「——Rich Hickey,Clojure 的創造者

可變狀態很棘手,而 OOP 共享可變狀態的引用(而非值)的作法讓這個問題更嚴重了。這意味着幾乎任何東西均可以改變給定對象的狀態。開發者必須牢記與當前對象交互的每一個對象的狀態,很快就會超過人腦工做記憶的上限。人腦要推理這種複雜的可變對象是極爲困難的。它消耗了寶貴且有限的認知資源,而且不可避免地會致使大量缺陷。

共享可變對象的引用是爲了提升效率而作出的權衡,過去這可能還很合理。但現在硬件性能飛速提高,咱們應該更加關注開發者的效率而不是代碼的執行效率。並且有了現代工具的支持,不變性幾乎不會影響性能。

OOP 說全局狀態是萬惡之源。但諷刺的是 OOP 程序基本上就是一個大型全局狀態(由於一切都是可變的而且經過引用共享)。

最小知識原則沒什麼用途,只是鴕鳥政策而已——無論你怎樣訪問或改變一個狀態,共享的可變狀態仍然是共享的可變狀態。領域驅動設計是一種有用的設計方法,能解決一些複雜度問題。但它仍然沒有解決不肯定性這個根本問題。

 信噪比

不少人都在關注 OOP 程序的不肯定性引入的複雜度。他們提出了許多設計模式試圖解決這些問題。但這只是自欺欺人,並引入了更加沒必要要的複雜度。

正如我以前所說,代碼自己是複雜度的最大來源,代碼老是越少越好。OOP 程序一般帶有大量的樣板代碼,以及設計模式提供的「創可貼」,這些都會下降信噪比。這意味着代碼變得更加冗長,人們更難看到程序的原始意圖,使代碼庫變得很是複雜,不太可靠。

我堅信現代 OOP 是軟件複雜度的最大來源之一。的確有使用 OOP 構建的成功項目,但這並不意味着此類項目不會受無謂的複雜度影響。

JavaScript 中的 OOP 尤爲糟糕,由於這種語言缺乏靜態類型檢查、泛型和接口等。JavaScript 中的 this 關鍵字至關不可靠。

若是咱們的目標是編寫可靠的軟件,那麼咱們應該努力下降複雜度,理想狀況下應該避免使用 OOP。若是你有興趣瞭解更多信息,請務必閱讀個人另外一篇文 「OOP,萬億美圓的災難」: https://medium.com/@ilyasz/object-oriented-programming-the-trillion-dollar-disaster-%EF%B8%8F-92a4b666c7c7

This 關鍵字

this 關鍵字的行爲老是飄忽不定。它很挑剔,在不一樣的環境中可能搞出來徹底不一樣的東西。它的行爲甚至取決於誰調用了一個給定的函數。使用 this 關鍵字常常會致使細小而奇怪的錯誤,很難調試。

拿它作面試問題可能頗有意思,但關於 this 關鍵字的知識其實也沒什麼意義,只能說明應聘者花了幾個小時研究過最多見的 JavaScript 面試問題。

真實世界的代碼不該該那麼容易出錯,應該是可讀的,不讓人感到莫名其妙。This 是一個明顯的語言設計缺陷,別再用它了。

建議的 ESLint 配置:

rules:
 fp/no-this: warn
聲明式代碼

聲明式編程是一個流行術語,咱們來看看它的實質和優勢。

若是你是編程老手,可能你一直在用命令式的編程風格,這種風格描述了一系列實現結果所需的步驟。相比之下聲明式風格是描述指望的結果,而不是具體的步驟。

典型的聲明式語言有 SQL 和 HTML。甚至包括 React 中的 JSX!

咱們不會指定具體的步驟來告訴數據庫如何獲取數據,而是使用 SQL 來描述要獲取的內容:

SELECT * FROM Users WHERE Country='USA';

在命令式 JavaScript 中這樣表示:

let user = null;

for (const u of users) {
 if (u.country === 'USA') {
   user = u;
   break;
 }
}

在聲明式 JavaScript 中使用實驗性流水線運算符(https://github.com/tc39/proposal-pipeline-operator):

import { filter, first } from 'lodash/fp';

const filterByCountry =
 country => filter( user => user.country === country );

const user =
 users
 |> filterByCountry('USA')
 |> first;

我以爲第二種方法看起來更簡潔,更具可讀性。

 優先使用表達式而非語句

編寫聲明式代碼時應優先使用表達式而非語句。表達式始終返回一個值,而語句是用來執行操做的,不返回任何結果。這在函數式編程中也稱爲「反作用」。順便說一句,前面討論的狀態突變也是反作用。

經常使用的語句有 if、return、switch、for、while。

來看一個簡單的例子:

const calculateStuff = input => {
 if (input.x) {
   return superCalculator(input.x);
 }

 return dumbCalculator(input.y);
};

這能夠很容易地重寫爲三元表達式(這是聲明式的):

const calculateStuff = input => {
 return input.x
         ? superCalculator(input.x)
         : dumbCalculator(input.y);
};

若是 lambda 函數中只有 return 語句,那麼 JavaScript 也容許咱們不用 lambda 語句:

const calculateStuff = input =>
 input.x ? superCalculator(input.x) : dumbCalculator(input.y);

函數長度從六行減到了一行。聲明式編程太有用了!

語句還會引發反作用和突變,進而產生不肯定性,下降代碼的可讀性和可靠性。從新排序語句是不安全的,由於它們的執行依賴編寫的順序。語句(包括循環)難以並行化,由於它們在其做用域以外突變狀態。使用語句會帶來更多複雜度,進而產生額外的頭腦負擔。

相比之下,表達式能夠安全地從新排序,不會產生反作用,易於並行化。

聲明式編程須要努力才能熟練

學習聲明式編程不是一蹴而就的,尤爲是多數人學的都是命令式編程。聲明式編程須要全新的思惟模式。要熟悉聲明式編程,學習使用沒有可變狀態的程序是一個好的開始——既不用 let 關鍵字,也不改變狀態。我能夠確定,熟悉聲明式編程後你的代碼會變得美觀優雅。

建議的 ESLint 配置:

rules:
 fp/no-let: warn
 fp/no-loops: warn
 fp/no-mutating-assign: warn
 fp/no-mutating-methods: warn
 fp/no-mutation: warn
 fp/no-delete: warn
避免將多個參數傳遞給函數

JavaScript 不是靜態類型語言,沒法保證函數使用正確和符合預期的參數來調用。ES6 引入了許多出色的功能,解構對象就是其中之一,它也可用於函數參數。

下面的代碼很直觀嗎?你能馬上說出參數是什麼嗎?我反正不能。

const total = computeShoppingCartTotal(itemList, 10.0, 'USD');

下面的例子呢?

const computeShoppingCartTotal = ({ itemList, discount, currency }) => {...};

const total = computeShoppingCartTotal({ itemList, discount: 10.0, currency: 'USD' });

顯而後者比前者更具可讀性。從不一樣模塊發起的函數調用尤爲符合這種狀況。使用參數對象還能讓參數不受編寫順序的影響。

建議的 ESLint 配置:

rules:
 max-params:
 - warn
 - 2
優先從函數返回對象

下面這段代碼的函數簽名是什麼?它返回了什麼?是返回用戶對象?用戶 ID?操做狀態?不看上下文很難回答。

const result = saveUser(...);

從函數返回一個對象能明確開發者的意圖,使代碼更易讀:

const { user, status } = saveUser(...);

...

const saveUser = user => {
  ...

  return {
    user: savedUser,
    status: "ok"
  };
};
控制執行流程中的異常

咱們常常會遇到莫名其妙的錯誤,錯誤信息什麼細節都沒有。雖然說老師教咱們在發生意外狀況時拋出異常,但這並非處理錯誤的最佳方法。

 異常破壞了類型安全

即便在靜態類型語言中,異常也會破壞類型安全性。根據其簽名所示,函數 fetchUser(id: number): User 應該返回一個用戶。函數簽名沒說若是找不到用戶就拋出異常。若是須要異常,那麼更合適的函數簽名是:fetchUser(...): User|throws UserNotFoundError。固然這種語法在任何語言中都是無效的。

推理程序的異常是很難的——人們可能永遠不會知道函數是否會拋出異常。咱們是能夠把函數調用都包裝在 try/catch 塊中,但這不怎麼實用,而且會嚴重影響代碼的可讀性。

 異常破壞了函數組合

異常使函數組合難以利用。下面的例子中若是某篇帖子沒法獲取,服務器將返回 500 內部服務器錯誤。

const fetchBlogPost = id => {
 const post = api.fetch(`/api/post/${id}`);

 if (!post) throw new Error(`Post with id ${id} not found`);

 return post;
};

const html = postIds |> map(fetchBlogPost) |> renderHTMLTemplate;

若是其中一個帖子被刪除,但因爲一些模糊的 bug,用戶仍然試圖訪問它怎麼辦?這將顯著下降用戶體驗。

 用元組處理錯誤

一種簡單的錯誤處理方法是返回包含結果和錯誤的元組,而不是拋出異常。JavaScript 的確不支持元組,但可使用 [error,result] 形式的雙值數組很容易地模擬它們。順便說一下,這也是 Go 中錯誤處理的默認方法:

const fetchBlogPost = id => {
 const post = api.fetch(`/api/post/${id}`);

 return post
     // null for error if post was found
   ?  [null, post]
     // null for result if post was not found
   :  [`Post with id ${id} not found`, null];
};

const blogPosts = postIds |> map(fetchBlogPost);

const errors =
 blogPosts
 |> filter(([err]) => !!err)  // keep only items with errors
 |> map(([err]) => err); // destructure the tuple and return the error

const html =
 blogPosts
 |> filter(([err]) => !err)  // keep only items with no errors
 |> map(([_, result]) => result)  // destructure the tuple and return the result
 |> renderHTML;
 有時異常也有用途

異常仍然在開發中佔有一席之地。一個簡單的原則是你來問本身一個問題——我是否能接受程序崩潰?拋出的任何異常均可能摧毀整個流程。就算咱們仔細考慮了全部極端狀況,異常仍然是不安全的,早晚讓程序崩潰。只有在你能接受程序崩潰時才拋出異常,好比說開發者錯誤或數據庫鏈接失敗等。

所謂異常,只應該用在出現例外狀況,程序別無選擇只能崩潰的時候。爲了控制執行流程應該儘可能避免拋出和捕獲異常。

 讓它崩潰——避免捕獲異常

因而就能夠總結出處理錯誤的終極規則——避免捕獲異常。若是咱們打算讓程序崩潰就能夠拋出錯誤,但永遠不該該捕獲這些錯誤。這也是 Haskell 和 Elixir 等函數式語言推薦的方法。

惟一例外是使用第三方 API 的狀況。即便在這種狀況下也最好仍是使用包裝函數的輔助函數來返回 [error,result] 元組代替異常。你可使用像 saferr 這樣的工具。

問問本身誰應該對錯誤負責。若是答案是用戶,則應該正常處理錯誤。咱們應該向用戶顯示友好的消息,而不是什麼 500 內部服務器錯誤。

惋惜這裏沒有 no-try-catch ESLint 規則。最接近的是 no-throw 規則。出現特殊狀況時,你拋出異常就應該預料到程序的崩潰。

建議的 ESLint 配置:

rules:
 fp/no-throw: warn
部分應用函數

部分應用函數(Partial function application)多是史上最佳的代碼共享機制之一。它擺脫了 OOP 依賴注入。你無需使用典型的 OOP 樣板也能在代碼中注入依賴項。

如下示例包裝了因拋出異常(而不是返回失敗的響應)而臭名昭著的 Axios 庫)。這些庫根本不必,尤爲是在使用 async/await 時。

下面的例子中咱們使用 currying 和部分應用函數來保證一個不安全函數的安全性。

// Wrapping axios to safely call the api without throwing exceptions
const safeApiCall = ({ url, method }) => data =>
 axios({ url, method, data })
   .then( result => ([null, result]) )
   .catch( error => ([error, null]) );

// Partially applying the generic function above to work with the users api
const createUser = safeApiCall({
   url: '/api/users',
   method: 'post'
 });

// Safely calling the api without worrying about exceptions.
const [error, user] = await createUser({
 email: 'ilya@suzdalnitski.com',
 password: 'Password'
});

注意,safeApiCall 函數寫爲 func = (params) => (data) => {...}。這是函數式編程中的經常使用技術,稱爲 currying;它與部分應用函數關係密切。使用 params 調用時,func 函數返回另外一個實際執行做業的函數。換句話說,該函數部分應用了 params。

它也能夠寫成:

const func = (params) => (
  (data) => {...}
);

請注意,依賴項(params)做爲第一個參數傳遞,實際數據做爲第二個參數傳遞。

爲了簡化操做你可使用 saferr npm 包,它也適用於 promise 和 async/await:

import saferr from "saferr";
import axios from "axios";

const safeGet = saferr(axios.get);

const testAsync = async url => {
 const [err, result] = await safeGet(url);

 if (err) {
   console.error(err.message);
   return;
 }

 console.log(result.data.results[0].email);
};


// prints: zdenka.dieckmann@example.com
testAsync("https://randomuser.me/api/?results=1");

// prints: Network Error
testAsync("https://shmoogle.com");
幾個小技巧

列舉一些方便的小技巧。它們不必定讓代碼更可靠,但可讓咱們的工做更輕鬆。有些技巧廣爲人知,有些否則。

 來一點類型安全

JavaScript 不是靜態類型語言。但咱們能夠按需標記函數參數來使代碼更加健壯。下面的代碼中,所需的值沒能傳入時將拋出錯誤。請注意它不適用於空值,但仍然能夠很好地防範未定義的值。

const req = name => {
 throw new Error(`The value ${name} is required.`);
};

const doStuff = ( stuff = req('stuff') ) => {
 ...
}
 短路條件和評估

你們都熟悉短路條件,它能用來訪問嵌套對象中的值。

const getUserCity = user =>
 user && user.address && user.address.city;

const user = {
 address: {
   city: "San Francisco"
 }
};

// Returns "San Francisco"
getUserCity(user);

// Both return undefined
getUserCity({});
getUserCity();

若是值爲虛值(falsey),那麼短路評估能夠用來提供替代值:

const userCity = getUserCity(user) || "Detroit";
 賦值兩次

給值賦值兩次能夠將任何值轉換爲布爾值。請注意,任何虛值都將轉換爲 false,這可能並不老是你想要的。數字毫不能這樣作,由於 0 也將被轉換爲 false。

const shouldShowTooltip = text => !!text;

// returns true
shouldShowTooltip('JavaScript rocks');

// all return false
shouldShowTooltip('');
shouldShowTooltip(null);
shouldShowTooltip();
 使用現場日誌來調試

咱們能夠利用短路和 console.log 的虛值輸出來調試函數代碼,甚至 React 組件:

const add = (a, b) =>
 console.log('add', a, b)
 || (a + b);

const User = ({email, name}) => (
 <>
   <Email value={console.log('email', email) || email} />
   <Name value={console.log('name', name) || name} />
 </>
);
    總結   

你真的須要代碼可靠性嗎?答案取決於你本身的決定。你的組織是否定爲開發者的效率取決於完成的 JIRA 故事?大家是否是所謂的函數工廠,工做只是生產更多的函數?若是是這樣的話仍是換個工做吧。

本文的內容在實踐中很是有用,值得反覆閱讀。好好看看這些技巧,ESLint 規則也都試一試吧。

英文原文: https://medium.com/better-programming/js-reliable-fdea261012ee

 活動推薦

GMTC 全球大前端技術大會首次落地華南,走入大灣區深圳。

往屆咱們請到了來自 Google、Twitter、Ins、阿里、騰訊、字節跳動、百度、京東、美團等國內外一線公司的頂級前端技術專家,分享了關於小程序、Flutter、Node、RN、前端框架、前端安全、前端工程化等 50 多場技術乾貨。今年深圳大會 7 折售票通道已經開啓,詳情諮詢:13269078023(同微信)。

閱讀原文閱讀 7904分享收藏在看35寫下你的留言精選留言

  • 07aa5766a4411b3e98840dfffe1be502.jpeg楓梓2Go 那樣的錯誤反回?Oh 饒了我吧!
  • 40c9954943317e02563c67440e69b6a9.jpeg這一年1對於let的疑問,做者初衷應該是想養成一個良好的聲明習慣,方便在不當心篡改變量時形成對程序的影響,相似在java中不常修改的變量聲明時儘可能採用finally同樣
  • de6b865001894e872ffb87dd28acd710.jpegMamba1好文!儘可能不使用let讓我大開眼界
  • 4bab94efdebc42e251ec5710f121d295.jpeg小鑫1不一樣意,不要 push 數組。 應該在拷貝時候 const b = [...a]; 而不是在修改的時候
  • 9239b80c8c31e59a38089c4ef1bff9a6.jpegRyn1let 的意義何在。沒有場景嗎
相關文章
相關標籤/搜索