關鍵要點
本文由 Matt Burnett、Simon Codrington 和 Nilson Jacques 共同評審。感謝所有 SitePoint 的同行評審者,使 SitePoint 內容達到最佳狀態!
您是否曾經在一次運行中完成一個項目,而無需再次查看代碼?我也沒有。在處理舊項目時,您可能希望花費很少或根本不花時間來弄清楚代碼的工作原理。可讀性強的代碼對於保持產品的可維護性以及讓您和您的同事或合作者滿意至關重要。
在JS1k 競賽中可以找到難以閱讀代碼的誇張示例,其目標是以1024 個字符或更少的字符編寫最佳JavaScript 應用程序,以及JSF*ck(順便說一下,NSFW),這是一種深奧的編程風格,僅使用六個不同的字符來編寫JavaScript 代碼。查看這些網站上的代碼會讓您想知道發生了什麼。想像一下編寫這樣的代碼並在幾個月後嘗試修復錯誤。
如果您定期瀏覽互聯網或構建界面,您可能會知道,退出大型、笨重的表單比退出看起來簡單而小的表單更容易。代碼也是如此。當被認為更容易閱讀和使用時,人們可能會更喜歡使用它。至少它會避免您因沮喪而扔掉電腦。
在本文中,我將探討使代碼更易於閱讀的技巧和竅門,以及要避免的陷阱。
堅持表單類比,表單有時會分成幾部分,使其看起來不那麼困難。代碼也可以這樣做。通過將其分成幾部分,讀者可以跳到與他們相關的部分,而不是費力地瀏覽叢林。
多年來,我們一直在為網絡優化各種事物。 JavaScript 文件也不例外。想想縮小和預 HTTP/2,我們通過將腳本組合成一個來節省 HTTP 請求。今天,我們可以按照自己的意願工作,並使用像 Gulp 或 Grunt 這樣的任務運行器來處理我們的文件。可以肯定地說,我們可以按照自己喜歡的方式進行編程,並將優化(例如連接)留給工具。
// 从 API 加载用户数据 var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { // 对用户执行某些操作 }); getUsersRequest.send(); //--------------------------------------------------- // 不同的功能从这里开始。也许 // 这是一个分成文件的时机。 //--------------------------------------------------- // 从 API 加载帖子数据 var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { // 对帖子执行某些操作 }); getPostsRequest.send();
函數允許我們創建可以重用的代碼塊。通常,函數的內容是縮進的,因此很容易看到函數的起始位置和結束位置。一個好習慣是保持函數很小——10 行或更少。當函數命名正確時,也很容易理解調用函數時發生了什麼。稍後我們將介紹命名約定。
// 从 API 加载用户数据 function getUsers(callback) { var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { callback(JSON.parse(getUsersRequest.responseText)); }); getUsersRequest.send(); } // 从 API 加载帖子数据 function getPosts(callback) { var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { callback(JSON.parse(getPostsRequest.responseText)); }); getPostsRequest.send(); } // 由于命名正确,因此无需阅读实际函数即可轻松理解此代码 // getUsers(function(users) { // // 对用户执行某些操作 // }); // getPosts(function(posts) { // // 对帖子执行某些操作 // });
我們可以簡化上面的代碼。注意這兩個函數幾乎完全相同嗎?我們可以應用“不要重複自己”(DRY)原則。這可以防止混亂。
function fetchJson(url, callback) { var request = new XMLHttpRequest(); request.open('GET', url, true); request.addEventListener('load', function() { callback(JSON.parse(request.responseText)); }); request.send(); } // 下面的代码仍然很容易理解 // 无需阅读上面的函数 fetchJson('/api/users', function(users) { // 对用户执行某些操作 }); fetchJson('/api/posts', function(posts) { // 对帖子执行某些操作 });
如果我們想通過 POST 請求創建一個新用戶怎麼辦?此時,一種選擇是向函數添加可選參數,從而向函數引入新的邏輯,使其過於復雜而無法成為一個函數。另一種選擇是專門為 POST 請求創建一個新函數,這將導致代碼重複。
我們可以通過面向對象編程獲得兩者的優點,允許我們創建一個可配置的單次使用對象,同時保持其可維護性。
注意:如果您需要專門關於面向對象 JavaScript 的入門知識,我推薦這段視頻:面向對象 JavaScript 的權威指南
考慮對象,通常稱為類,它們是一組上下文感知的函數。一個對象非常適合放在專用文件中。在我們的例子中,我們可以為 XMLHttpRequest 構建一個基本的包裝器。
HttpRequest.js
function HttpRequest(url) { this.request = new XMLHttpRequest(); this.body = undefined; this.method = HttpRequest.METHOD_GET; this.url = url; this.responseParser = undefined; } HttpRequest.METHOD_GET = 'GET'; HttpRequest.METHOD_POST = 'POST'; HttpRequest.prototype.setMethod = function(method) { this.method = method; return this; }; HttpRequest.prototype.setBody = function(body) { if (typeof body === 'object') { body = JSON.stringify(body); } this.body = body; return this; }; HttpRequest.prototype.setResponseParser = function(responseParser) { if (typeof responseParser !== 'function') return; this.responseParser = responseParser; return this; }; HttpRequest.prototype.send = function(callback) { this.request.addEventListener('load', function() { if (this.responseParser) { callback(this.responseParser(this.request.responseText)); } else { callback(this.request.responseText); } }, false); this.request.open(this.method, this.url, true); this.request.send(this.body); return this; };
app.js
new HttpRequest('/users') .setResponseParser(JSON.parse) .send(function(users) { // 对用户执行某些操作 }); new HttpRequest('/posts') .setResponseParser(JSON.parse) .send(function(posts) { // 对帖子执行某些操作 }); // 创建一个新用户 new HttpRequest('/user') .setMethod(HttpRequest.METHOD_POST) .setBody({ name: 'Tim', email: 'info@example.com' }) .setResponseParser(JSON.parse) .send(function(user) { // 对新用户执行某些操作 });
上面創建的 HttpRequest 類現在非常可配置,因此可以應用於我們的許多 API 調用。儘管實現(一系列鍊式方法調用)更複雜,但類的功能易於維護。在實現和可重用性之間取得平衡可能很困難,並且是特定於項目的。
使用 OOP 時,設計模式是一個很好的補充。雖然它們本身不會提高可讀性,但一致性會!
文件、函數、對象,這些只是粗略的線條。它們使您的代碼易於掃描。使代碼易於閱讀是一種更為細緻的藝術。最細微的細節都會產生重大影響。例如,將您的行長限制為 80 個字符是一個簡單的解決方案,通常通過垂直線由編輯器強制執行。但還有更多!
適當的命名可以導致即時識別,從而無需查找值是什麼或函數的作用。
函數通常採用駝峰式命名法。以動詞開頭,然後是主語通常會有所幫助。
// 从 API 加载用户数据 var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { // 对用户执行某些操作 }); getUsersRequest.send(); //--------------------------------------------------- // 不同的功能从这里开始。也许 // 这是一个分成文件的时机。 //--------------------------------------------------- // 从 API 加载帖子数据 var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { // 对帖子执行某些操作 }); getPostsRequest.send();
對於變量名,嘗試應用倒金字塔方法。主題放在前面,屬性放在後面。
// 从 API 加载用户数据 function getUsers(callback) { var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { callback(JSON.parse(getUsersRequest.responseText)); }); getUsersRequest.send(); } // 从 API 加载帖子数据 function getPosts(callback) { var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { callback(JSON.parse(getPostsRequest.responseText)); }); getPostsRequest.send(); } // 由于命名正确,因此无需阅读实际函数即可轻松理解此代码 // getUsers(function(users) { // // 对用户执行某些操作 // }); // getPosts(function(posts) { // // 对帖子执行某些操作 // });
能夠區分普通變量和特殊變量也很重要。例如,常量的名稱通常以大寫字母編寫,並帶有下劃線。
function fetchJson(url, callback) { var request = new XMLHttpRequest(); request.open('GET', url, true); request.addEventListener('load', function() { callback(JSON.parse(request.responseText)); }); request.send(); } // 下面的代码仍然很容易理解 // 无需阅读上面的函数 fetchJson('/api/users', function(users) { // 对用户执行某些操作 }); fetchJson('/api/posts', function(posts) { // 对帖子执行某些操作 });
類通常採用駝峰式命名法,以大寫字母開頭。
function HttpRequest(url) { this.request = new XMLHttpRequest(); this.body = undefined; this.method = HttpRequest.METHOD_GET; this.url = url; this.responseParser = undefined; } HttpRequest.METHOD_GET = 'GET'; HttpRequest.METHOD_POST = 'POST'; HttpRequest.prototype.setMethod = function(method) { this.method = method; return this; }; HttpRequest.prototype.setBody = function(body) { if (typeof body === 'object') { body = JSON.stringify(body); } this.body = body; return this; }; HttpRequest.prototype.setResponseParser = function(responseParser) { if (typeof responseParser !== 'function') return; this.responseParser = responseParser; return this; }; HttpRequest.prototype.send = function(callback) { this.request.addEventListener('load', function() { if (this.responseParser) { callback(this.responseParser(this.request.responseText)); } else { callback(this.request.responseText); } }, false); this.request.open(this.method, this.url, true); this.request.send(this.body); return this; };
一個小細節是縮寫。有些人選擇將縮寫全部大寫,而另一些人則選擇堅持使用駝峰式命名法。使用前者可能會使識別後續縮寫變得更加困難。
在許多代碼庫中,您可能會遇到一些“特殊”代碼來減少字符數或提高算法的性能。
單行代碼是簡潔代碼的一個示例。不幸的是,它們通常依賴於技巧或晦澀的語法。下面看到的嵌套三元運算符就是一個常見的例子。儘管它很簡潔,但與普通的 if 語句相比,理解它的作用也可能需要一秒或兩秒鐘。小心使用語法快捷方式。
new HttpRequest('/users') .setResponseParser(JSON.parse) .send(function(users) { // 对用户执行某些操作 }); new HttpRequest('/posts') .setResponseParser(JSON.parse) .send(function(posts) { // 对帖子执行某些操作 }); // 创建一个新用户 new HttpRequest('/user') .setMethod(HttpRequest.METHOD_POST) .setBody({ name: 'Tim', email: 'info@example.com' }) .setResponseParser(JSON.parse) .send(function(user) { // 对新用户执行某些操作 });
微優化是性能優化,通常影響很小。大多數情況下,它們不如性能較低的等效項易於閱讀。
function getApiUrl() { /* ... */ } function setRequestMethod() { /* ... */ } function findItemsById(n) { /* ... */ } function hideSearchForm() { /* ... */ }
JavaScript 編譯器非常擅長為我們優化代碼,而且它們還在不斷改進。除非未優化代碼和優化代碼之間的差異很明顯(通常在數千或數百萬次操作之後),否則建議選擇更容易閱讀的代碼。
這具有諷刺意味,但保持代碼可讀性的更好方法是添加不會執行的語法。讓我們稱之為非代碼。
我很確定每個開發人員都曾有過其他開發人員提供,或者檢查過某個網站的壓縮代碼——其中大多數空格都被刪除的代碼。第一次遇到這種情況可能會令人相當驚訝。在不同的視覺藝術領域,如設計和排版,空白與填充一樣重要。您需要找到兩者之間的微妙平衡。對這種平衡的看法因公司、團隊和開發人員而異。幸運的是,有一些普遍認同的規則:
任何其他規則都應與您合作的任何人討論。無論您同意哪種代碼風格,一致性都是關鍵。
var element = document.getElementById('body'), elementChildren = element.children, elementChildrenCount = elementChildren.length; // 定义一组颜色时,我在变量前加“color”前缀 var colorBackground = 0xFAFAFA, colorPrimary = 0x663399; // 定义一组背景属性时,我使用 background 作为基准 var backgroundColor = 0xFAFAFA, backgroundImages = ['foo.png', 'bar.png']; // 上下文可以改变一切 var headerBackgroundColor = 0xFAFAFA, headerTextColor = 0x663399;
與空格一樣,註釋可以成為為代碼提供一些空間的好方法,還可以讓您向代碼添加詳細信息。請務必添加註釋以顯示:
var URI_ROOT = window.location.href;
並非所有修復都是顯而易見的。添加其他信息可以闡明很多內容:
// 从 API 加载用户数据 var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { // 对用户执行某些操作 }); getUsersRequest.send(); //--------------------------------------------------- // 不同的功能从这里开始。也许 // 这是一个分成文件的时机。 //--------------------------------------------------- // 从 API 加载帖子数据 var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { // 对帖子执行某些操作 }); getPostsRequest.send();
編寫面向對象軟件時,內聯文檔與普通註釋一樣,可以為代碼提供一些呼吸空間。它們還有助於闡明屬性或方法的目的和細節。許多 IDE 將它們用於提示,生成的文檔工具也使用它們!無論原因是什麼,編寫文檔都是一項極好的實踐。
// 从 API 加载用户数据 function getUsers(callback) { var getUsersRequest = new XMLHttpRequest(); getUsersRequest.open('GET', '/api/users', true); getUsersRequest.addEventListener('load', function() { callback(JSON.parse(getUsersRequest.responseText)); }); getUsersRequest.send(); } // 从 API 加载帖子数据 function getPosts(callback) { var getPostsRequest = new XMLHttpRequest(); getPostsRequest.open('GET', '/api/posts', true); getPostsRequest.addEventListener('load', function() { callback(JSON.parse(getPostsRequest.responseText)); }); getPostsRequest.send(); } // 由于命名正确,因此无需阅读实际函数即可轻松理解此代码 // getUsers(function(users) { // // 对用户执行某些操作 // }); // getPosts(function(posts) { // // 对帖子执行某些操作 // });
事件和異步調用是 JavaScript 的強大功能,但它通常會使代碼更難以閱讀。
異步調用通常使用回調提供。有時,您希望按順序運行它們,或者等待所有異步調用準備好。
function fetchJson(url, callback) { var request = new XMLHttpRequest(); request.open('GET', url, true); request.addEventListener('load', function() { callback(JSON.parse(request.responseText)); }); request.send(); } // 下面的代码仍然很容易理解 // 无需阅读上面的函数 fetchJson('/api/users', function(users) { // 对用户执行某些操作 }); fetchJson('/api/posts', function(posts) { // 对帖子执行某些操作 });
Promise 對像在 ES2015(也稱為 ES6)中引入,用於解決這兩個問題。它允許您展平嵌套的異步請求。
function HttpRequest(url) { this.request = new XMLHttpRequest(); this.body = undefined; this.method = HttpRequest.METHOD_GET; this.url = url; this.responseParser = undefined; } HttpRequest.METHOD_GET = 'GET'; HttpRequest.METHOD_POST = 'POST'; HttpRequest.prototype.setMethod = function(method) { this.method = method; return this; }; HttpRequest.prototype.setBody = function(body) { if (typeof body === 'object') { body = JSON.stringify(body); } this.body = body; return this; }; HttpRequest.prototype.setResponseParser = function(responseParser) { if (typeof responseParser !== 'function') return; this.responseParser = responseParser; return this; }; HttpRequest.prototype.send = function(callback) { this.request.addEventListener('load', function() { if (this.responseParser) { callback(this.responseParser(this.request.responseText)); } else { callback(this.request.responseText); } }, false); this.request.open(this.method, this.url, true); this.request.send(this.body); return this; };
儘管我們引入了其他代碼,但這更容易正確解釋。您可以在此處閱讀更多關於 Promise 的信息:JavaScript 變得異步(而且很棒)
如果您了解 ES2015 規範,您可能已經註意到本文中的所有代碼示例都是舊版本的(Promise 對象除外)。儘管 ES6 為我們提供了強大的功能,但在可讀性方面還是有一些問題。
胖箭頭語法定義了一個函數,該函數從其父作用域繼承 this 的值。至少,這就是它被設計的原因。使用它來定義常規函數也很誘人。
new HttpRequest('/users') .setResponseParser(JSON.parse) .send(function(users) { // 对用户执行某些操作 }); new HttpRequest('/posts') .setResponseParser(JSON.parse) .send(function(posts) { // 对帖子执行某些操作 }); // 创建一个新用户 new HttpRequest('/user') .setMethod(HttpRequest.METHOD_POST) .setBody({ name: 'Tim', email: 'info@example.com' }) .setResponseParser(JSON.parse) .send(function(user) { // 对新用户执行某些操作 });
另一個示例是 rest 和 spread 語法。
function getApiUrl() { /* ... */ } function setRequestMethod() { /* ... */ } function findItemsById(n) { /* ... */ } function hideSearchForm() { /* ... */ }
我的意思是,ES2015 規範引入許多有用但晦澀、有時令人困惑的語法,這使得它容易被濫用於單行代碼。我不希望阻止使用這些功能。我希望鼓勵謹慎使用它們。
在項目的每個階段,都要記住保持代碼的可讀性和可維護性。從文件系統到微小的語法選擇,一切都很重要。尤其是在團隊中,很難始終強制執行所有規則。代碼審查可以提供幫助,但仍然存在人為錯誤的餘地。幸運的是,有一些工具可以幫助您做到這一點!
除了代碼質量和样式工具之外,還有一些工具可以使任何代碼更易於閱讀。嘗試不同的語法高亮主題,或嘗試使用小地圖來查看腳本的自上而下的概述(Atom、Brackets)。
您對編寫可讀且可維護的代碼有何看法?我很想在下面的評論中聽到您的想法。
代碼的可讀性至關重要,原因如下。首先,它使代碼更容易理解、調試和維護。當代碼可讀時,其他開發人員更容易理解代碼的作用,這在協作環境中尤其重要。其次,可讀性強的代碼更有可能正確。如果開發人員可以輕鬆理解代碼,那麼他們在修改代碼時不太可能引入錯誤。最後,可讀性強的代碼更容易測試。如果代碼清晰簡潔,則更容易確定需要測試的內容以及如何測試。
如果編程語言具有清晰簡潔的語法、使用有意義的標識符以及包含解釋代碼作用的註釋,則該語言被認為易於閱讀。像 Python 和 Ruby 這樣的高級語言通常被認為易於閱讀,因為它們使用類似英語的語法並允許使用清晰的、描述性的變量名。但是,也可以通過良好的編碼實踐(例如一致的縮進、使用空格和全面的註釋)來提高像 C 或 Java 這樣的低級語言的可讀性。
函數可以通過允許開發人員重用代碼來顯著減少代碼量。與其多次編寫相同的代碼,不如編寫一次函數,然後在需要執行特定任務時調用該函數。這不僅使代碼更短、更易於閱讀,而且還使代碼更容易維護和調試,因為任何更改只需要在一個地方進行。
機器代碼是最低級的編程語言,由可以直接由計算機中央處理器 (CPU) 執行的二進制代碼組成。另一方面,高級語言更接近人類語言,需要在執行之前由編譯器或解釋器將其轉換為機器代碼。高級語言通常更容易閱讀和編寫,並且它們提供了更多與硬件的抽象,使它們更易於在不同類型的機器之間移植。
解釋器和編譯器是將高級語言轉換為機器代碼的工具。解釋器逐行翻譯和執行代碼,這允許交互式編碼和調試。但是,這可能比編譯代碼慢。另一方面,編譯器會在執行之前將整個程序轉換為機器代碼,這可以提高執行速度。但是,任何代碼錯誤都只有在編譯整個程序後才能發現。
彙編語言是一種低級編程語言,它使用助記符代碼來表示機器代碼指令。每種彙編語言都特定於特定的計算機體系結構。雖然它比機器代碼更易於閱讀,但它仍然比高級語言更難閱讀和編寫。但是,它允許直接控制硬件,這在某些情況下非常有用。
有幾種方法可以提高代碼的可讀性。這些方法包括使用有意義的變量和函數名、一致地縮進代碼、使用空格分隔代碼的不同部分以及包含解釋代碼作用的註釋。遵循您使用的編程語言的約定和最佳實踐也很重要。
註釋在使代碼可讀方面起著至關重要的作用。它們提供了對代碼作用、做出某些決策的原因以及復雜代碼部分如何工作的解釋。這對於需要理解和使用您的代碼的其他開發人員來說可能非常有幫助。但是,重要的是要使註釋簡潔且相關,並在代碼更改時更新它們。
可讀性強的代碼極大地促進了協作。當代碼易於閱讀時,其他開發人員更容易理解和參與貢獻。這在大型項目中尤其重要,在大型項目中,多個開發人員正在處理代碼庫的不同部分。可讀性強的代碼還可以更容易地讓新的團隊成員加入,因為他們可以快速了解代碼的作用以及它的工作原理。
可讀性強的代碼可以顯著提高軟件質量。當代碼易於閱讀時,更容易發現和修復錯誤,並確保代碼正在執行其應執行的操作。它還可以使隨著時間的推移更容易維護和增強軟件,因為它清楚地說明了代碼的每一部分的作用。這可以導致更可靠、更高效和更強大的軟件。
以上是人類可以閱讀代碼的重要性的詳細內容。更多資訊請關注PHP中文網其他相關文章!