关键要点
本文由 Matt Burnett、Simon Codrington 和 Nilson Jacques 共同评审。感谢所有 SitePoint 的同行评审者,使 SitePoint 内容达到最佳状态!
您是否曾经在一次运行中完成一个项目,而无需再次查看代码?我也没有。在处理旧项目时,您可能希望花费很少或根本不花时间来弄清楚代码的工作原理。可读性强的代码对于保持产品的可维护性以及让您和您的同事或合作者满意至关重要。
在JS1k 竞赛中可以找到难以阅读代码的夸张示例,其目标是以1024 个字符或更少的字符编写最佳JavaScript 应用程序,以及JSF*ck(顺便说一下,NSFW),这是一种深奥的编程风格,仅使用六个不同的字符来编写JavaScript 代码。查看这些网站上的代码会让您想知道发生了什么。想象一下编写这样的代码并在几个月后尝试修复错误。
如果您定期浏览互联网或构建界面,您可能会知道,退出大型、笨重的表单比退出看起来简单而小的表单更容易。代码也是如此。当被认为更容易阅读和使用时,人们可能会更喜欢使用它。至少它会避免您因沮丧而扔掉电脑。
在本文中,我将探讨使代码更易于阅读的技巧和窍门,以及要避免的陷阱。
坚持表单类比,表单有时会分成几部分,使其看起来不那么困难。代码也可以这样做。通过将其分成几部分,读者可以跳到与他们相关的部分,而不是费力地浏览丛林。
多年来,我们一直在为网络优化各种事物。JavaScript 文件也不例外。想想缩小和预 HTTP/2,我们通过将脚本组合成一个来节省 HTTP 请求。今天,我们可以按照自己的意愿工作,并使用像 Gulp 或 Grunt 这样的任务运行器来处理我们的文件。可以肯定地说,我们可以按照自己喜欢的方式进行编程,并将优化(例如连接)留给工具。
<code class="language-javascript">// 从 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();</code>
函数允许我们创建可以重用的代码块。通常,函数的内容是缩进的,因此很容易看到函数的起始位置和结束位置。一个好习惯是保持函数很小——10 行或更少。当函数命名正确时,也很容易理解调用函数时发生了什么。稍后我们将介绍命名约定。
<code class="language-javascript">// 从 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) { // // 对帖子执行某些操作 // });</code>
我们可以简化上面的代码。注意这两个函数几乎完全相同吗?我们可以应用“不要重复自己”(DRY)原则。这可以防止混乱。
<code class="language-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) { // 对帖子执行某些操作 });</code>
如果我们想通过 POST 请求创建一个新用户怎么办?此时,一种选择是向函数添加可选参数,从而向函数引入新的逻辑,使其过于复杂而无法成为一个函数。另一种选择是专门为 POST 请求创建一个新函数,这将导致代码重复。
我们可以通过面向对象编程获得两者的优点,允许我们创建一个可配置的单次使用对象,同时保持其可维护性。
注意:如果您需要专门关于面向对象 JavaScript 的入门知识,我推荐这段视频:面向对象 JavaScript 的权威指南
考虑对象,通常称为类,它们是一组上下文感知的函数。一个对象非常适合放在专用文件中。在我们的例子中,我们可以为 XMLHttpRequest 构建一个基本的包装器。
HttpRequest.js
<code class="language-javascript">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; };</code>
app.js
<code class="language-javascript">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) { // 对新用户执行某些操作 });</code>
上面创建的 HttpRequest 类现在非常可配置,因此可以应用于我们的许多 API 调用。尽管实现(一系列链式方法调用)更复杂,但类的功能易于维护。在实现和可重用性之间取得平衡可能很困难,并且是特定于项目的。
使用 OOP 时,设计模式是一个很好的补充。虽然它们本身不会提高可读性,但一致性会!
文件、函数、对象,这些只是粗略的线条。它们使您的代码易于扫描。使代码易于阅读是一种更为细致的艺术。最细微的细节都会产生重大影响。例如,将您的行长限制为 80 个字符是一个简单的解决方案,通常通过垂直线由编辑器强制执行。但还有更多!
适当的命名可以导致即时识别,从而无需查找值是什么或函数的作用。
函数通常采用驼峰式命名法。以动词开头,然后是主语通常会有所帮助。
<code class="language-javascript">// 从 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();</code>
对于变量名,尝试应用倒金字塔方法。主题放在前面,属性放在后面。
<code class="language-javascript">// 从 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) { // // 对帖子执行某些操作 // });</code>
能够区分普通变量和特殊变量也很重要。例如,常量的名称通常以大写字母编写,并带有下划线。
<code class="language-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) { // 对帖子执行某些操作 });</code>
类通常采用驼峰式命名法,以大写字母开头。
<code class="language-javascript">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; };</code>
一个小细节是缩写。有些人选择将缩写全部大写,而另一些人则选择坚持使用驼峰式命名法。使用前者可能会使识别后续缩写变得更加困难。
在许多代码库中,您可能会遇到一些“特殊”代码来减少字符数或提高算法的性能。
单行代码是简洁代码的一个示例。不幸的是,它们通常依赖于技巧或晦涩的语法。下面看到的嵌套三元运算符就是一个常见的例子。尽管它很简洁,但与普通的 if 语句相比,理解它的作用也可能需要一秒或两秒钟。小心使用语法快捷方式。
<code class="language-javascript">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) { // 对新用户执行某些操作 });</code>
微优化是性能优化,通常影响很小。大多数情况下,它们不如性能较低的等效项易于阅读。
<code class="language-javascript">function getApiUrl() { /* ... */ } function setRequestMethod() { /* ... */ } function findItemsById(n) { /* ... */ } function hideSearchForm() { /* ... */ }</code>
JavaScript 编译器非常擅长为我们优化代码,而且它们还在不断改进。除非未优化代码和优化代码之间的差异很明显(通常在数千或数百万次操作之后),否则建议选择更容易阅读的代码。
这具有讽刺意味,但保持代码可读性的更好方法是添加不会执行的语法。让我们称之为非代码。
我很确定每个开发人员都曾有过其他开发人员提供,或者检查过某个网站的压缩代码——其中大多数空格都被删除的代码。第一次遇到这种情况可能会令人相当惊讶。在不同的视觉艺术领域,如设计和排版,空白与填充一样重要。您需要找到两者之间的微妙平衡。对这种平衡的看法因公司、团队和开发人员而异。幸运的是,有一些普遍认同的规则:
任何其他规则都应与您合作的任何人讨论。无论您同意哪种代码风格,一致性都是关键。
<code class="language-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;</code>
与空格一样,注释可以成为为代码提供一些空间的好方法,还可以让您向代码添加详细信息。请务必添加注释以显示:
<code class="language-javascript">var URI_ROOT = window.location.href;</code>
并非所有修复都是显而易见的。添加其他信息可以阐明很多内容:
<code class="language-javascript">// 从 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();</code>
编写面向对象软件时,内联文档与普通注释一样,可以为代码提供一些呼吸空间。它们还有助于阐明属性或方法的目的和细节。许多 IDE 将它们用于提示,生成的文档工具也使用它们!无论原因是什么,编写文档都是一项极好的实践。
<code class="language-javascript">// 从 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) { // // 对帖子执行某些操作 // });</code>
事件和异步调用是 JavaScript 的强大功能,但它通常会使代码更难以阅读。
异步调用通常使用回调提供。有时,您希望按顺序运行它们,或者等待所有异步调用准备好。
<code class="language-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) { // 对帖子执行某些操作 });</code>
Promise 对象在 ES2015(也称为 ES6)中引入,用于解决这两个问题。它允许您展平嵌套的异步请求。
<code class="language-javascript">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; };</code>
尽管我们引入了其他代码,但这更容易正确解释。您可以在此处阅读更多关于 Promise 的信息:JavaScript 变得异步(而且很棒)
如果您了解 ES2015 规范,您可能已经注意到本文中的所有代码示例都是旧版本的(Promise 对象除外)。尽管 ES6 为我们提供了强大的功能,但在可读性方面还是有一些问题。
胖箭头语法定义了一个函数,该函数从其父作用域继承 this 的值。至少,这就是它被设计的原因。使用它来定义常规函数也很诱人。
<code class="language-javascript">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) { // 对新用户执行某些操作 });</code>
另一个示例是 rest 和 spread 语法。
<code class="language-javascript">function getApiUrl() { /* ... */ } function setRequestMethod() { /* ... */ } function findItemsById(n) { /* ... */ } function hideSearchForm() { /* ... */ }</code>
我的意思是,ES2015 规范引入许多有用但晦涩、有时令人困惑的语法,这使得它容易被滥用于单行代码。我不希望阻止使用这些功能。我希望鼓励谨慎使用它们。
在项目的每个阶段,都要记住保持代码的可读性和可维护性。从文件系统到微小的语法选择,一切都很重要。尤其是在团队中,很难始终强制执行所有规则。代码审查可以提供帮助,但仍然存在人为错误的余地。幸运的是,有一些工具可以帮助您做到这一点!
除了代码质量和样式工具之外,还有一些工具可以使任何代码更易于阅读。尝试不同的语法高亮主题,或尝试使用小地图来查看脚本的自上而下的概述(Atom、Brackets)。
您对编写可读且可维护的代码有何看法?我很想在下面的评论中听到您的想法。
代码的可读性至关重要,原因如下。首先,它使代码更容易理解、调试和维护。当代码可读时,其他开发人员更容易理解代码的作用,这在协作环境中尤其重要。其次,可读性强的代码更有可能正确。如果开发人员可以轻松理解代码,那么他们在修改代码时不太可能引入错误。最后,可读性强的代码更容易测试。如果代码清晰简洁,则更容易确定需要测试的内容以及如何测试。
如果编程语言具有清晰简洁的语法、使用有意义的标识符以及包含解释代码作用的注释,则该语言被认为易于阅读。像 Python 和 Ruby 这样的高级语言通常被认为易于阅读,因为它们使用类似英语的语法并允许使用清晰的、描述性的变量名。但是,也可以通过良好的编码实践(例如一致的缩进、使用空格和全面的注释)来提高像 C 或 Java 这样的低级语言的可读性。
函数可以通过允许开发人员重用代码来显著减少代码量。与其多次编写相同的代码,不如编写一次函数,然后在需要执行特定任务时调用该函数。这不仅使代码更短、更易于阅读,而且还使代码更容易维护和调试,因为任何更改只需要在一个地方进行。
机器代码是最低级的编程语言,由可以直接由计算机中央处理器 (CPU) 执行的二进制代码组成。另一方面,高级语言更接近人类语言,需要在执行之前由编译器或解释器将其转换为机器代码。高级语言通常更容易阅读和编写,并且它们提供了更多与硬件的抽象,使它们更易于在不同类型的机器之间移植。
解释器和编译器是将高级语言转换为机器代码的工具。解释器逐行翻译和执行代码,这允许交互式编码和调试。但是,这可能比编译代码慢。另一方面,编译器会在执行之前将整个程序转换为机器代码,这可以提高执行速度。但是,任何代码错误都只有在编译整个程序后才能发现。
汇编语言是一种低级编程语言,它使用助记符代码来表示机器代码指令。每种汇编语言都特定于特定的计算机体系结构。虽然它比机器代码更易于阅读,但它仍然比高级语言更难阅读和编写。但是,它允许直接控制硬件,这在某些情况下非常有用。
有几种方法可以提高代码的可读性。这些方法包括使用有意义的变量和函数名、一致地缩进代码、使用空格分隔代码的不同部分以及包含解释代码作用的注释。遵循您使用的编程语言的约定和最佳实践也很重要。
注释在使代码可读方面起着至关重要的作用。它们提供了对代码作用、做出某些决策的原因以及复杂代码部分如何工作的解释。这对于需要理解和使用您的代码的其他开发人员来说可能非常有帮助。但是,重要的是要使注释简洁且相关,并在代码更改时更新它们。
可读性强的代码极大地促进了协作。当代码易于阅读时,其他开发人员更容易理解和参与贡献。这在大型项目中尤其重要,在大型项目中,多个开发人员正在处理代码库的不同部分。可读性强的代码还可以更容易地让新的团队成员加入,因为他们可以快速了解代码的作用以及它的工作原理。
可读性强的代码可以显著提高软件质量。当代码易于阅读时,更容易发现和修复错误,并确保代码正在执行其应执行的操作。它还可以使随着时间的推移更容易维护和增强软件,因为它清楚地说明了代码的每一部分的作用。这可以导致更可靠、更高效和更强大的软件。
以上是人类可以阅读代码的重要性的详细内容。更多信息请关注PHP中文网其他相关文章!