编写可维护的JavaScript

978-7-115-31008-8
作者: 【美】Nicholas C. Zakas
译者: 李晶郭凯张散集
编辑: 陈冀康

图书目录:

详情

本书向开发人员阐述了如何在团队开发中编写具备高可维护性的JavaScript代码,它详细说明了作为团队一分子,应该怎么写JavaScript。本书内容涵盖了编码风格、编程技巧、自动化、测试等几个方面,既包括具体风格和原则的介绍,也包括示例和技巧说明,最后还介绍了如何通过自动化的工具和方法来实现一致的编程风格。

图书摘要

版权信息

书名:编写可维护的JavaScript

ISBN:978-7-115-31008-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Nicholas C. Zakas

  译    李 晶 郭 凯 张散集

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2012 by O’Reilly Media, Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom

Press, 2013. Authorized translation of the English edition, 2012 O’Reilly Media, Inc., the owner

of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体字版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了《Make》杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版、在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


任何语言都需要强调编码风格的一致性。只要是团队开发,每个人都以相同方式编写代码就是至关重要的。这样大家才能方便地互相看懂和维护对方的代码。

本书向开发人员阐述了如何在团队开发中编写具备高可维护性的JavaScript代码,书中详细说明了作为团队一分子,应该怎么写JavaScript。本书内容涵盖了编码风格、编程技巧、自动化、测试等几方面,既包括具体风格和原则的介绍,也包括示例和技巧说明,最后还介绍了如何通过自动化的工具和方法来实现一致的编程风格。

本书作者Nicholas C. Zakas是顶级的Web技术专家,也是《JavaScript高级程序设计》一书的作者。他曾是Yahoo!的首席前端开发工程师,在完成了从一名“独行侠”到“团队精英”的蜕变后,他站在前端工程师的角度提炼出众多的最佳编程实践,其中包括很多业内权威所推崇的最佳法则,而这些宝贵经验正是本书的核心内容。

本书适合前端开发工程师、JavaScript程序员和学习JavaScript编程的读者阅读,也适合开发团队负责人、项目负责人阅读。运用本书中讲述的技巧和技术,可以使JavaScript团队编程从侠义的个人偏好的阴霾走出来,走向真正的高可维护性、高效能和高水准。


Nicholas C. Zakas是一名前端开发顾问、作者和演讲家。在Yahoo!供职超过5年时间,在这期间他曾是Yahoo!首页首席前端工程师和YUI库代码贡献者。他著有《JavaScript高级程序设计》、《Ajax高级程序设计》和《高性能JavaScript》等书籍。Zakas倡导了很多最佳实践,包括渐进增强、可访问性、性能、扩展性和可维护性等。他的博客地址是:http://www.nczonline.net/。他的Twitter是:@slicknet。


在我的编程生涯中,曾遇到过各种各样的开发者,他们的编程风格天马行空,有时甚至让人哭笑不得。有一种风格被称为“霰弹枪编程”,例如某个方法调用出错了,我尝试将参数0改为'0'、NaN甚至false,直到试出能“正确”运行的参数为止。当你和这种人组成团队一起编程时,你会发现你的智商变得很低。

比“霰弹枪编程”更温柔一点的编程方式是“撞大运编程”,就是我根本看不懂程序到底在干嘛,但确实能正常运行,这往往是因为这些程序中有很多错误成对出现,于是就负负得正,看起来就正确了,这种程序实在是“动弹不得”,只能重构。当你和这种人组成团队时,上帝都会同情你。

当然,当渐渐意识到这类随意编程风格带来的危害时,很多人开始思考什么才是“好”的编程风格。不少人开始向高手学习,尽管有时并不知道高手为什么要把代码写成这个样子。于是越来越多的hack代码出现了,那些看起来晦涩难懂、短小精悍却又暂时行之有效的代码片段越来越流行,尤其是在处理浏览器兼容性问题时,这种情况更甚。有些人会在这些hack代码片段旁边打上记号,以便以后有问题时能留意到此。这时,问题又出现了,不同人做记号的方法又不一样,我的天哪!

如果你自诩为一名有能力有良知的程序员,遇到这种“烂”代码时往往将之重构,为了修改几个拼写错误的bug,而修改10个类,并且重构与这10个类有关联的另外20个类,甚至修改了打包脚本以及部署配置文件。这就是一种有着代码洁癖的人很“青睐”的编程风格——“屠宰式编程”。

霰弹枪式、撞大运式、不求甚解式、屠宰式……

编程是一项复杂的工程,却又如此充满喜感,让人又爱又恨。但有一点确定无疑,即这些风格因为缺少基本的约束,会导致团队协作效率低下,甚至影响产品的存亡。而对于Web开发领域最为流行却有着先天设计缺陷的语言JavaScript来说,情况更加糟糕。一直以来都缺少宏观的设计模式和微观的编程风格的指导,从而导致JavaScript编程始终没有权威和统一的指导思想和方法论。因此,大部分Web前端团队依然将很大精力放在解决注入代码冲突、命名规范性、代码复用模式等团队编程最基本的问题上。迟迟走不上创新、高效的快车道。

我们很欣喜地看到,在设计模式领域,《JavaScript设计模式》(JavaScript Design Patterns)和《JavaScript编程模式》(JavaScript Patterns)两本书填补了这方面的空白,而在编程风格领域,这本《编写可维护的JavaScript》(Maintainable JavaScript)真可谓姗姗来迟。

本书正是向开发人员阐述如何在团队开发中编写高可维护的JavaScript代码,涵盖了编码风格、编程技巧、自动化、测试等几方面,不过,同样的原则也适用于其他任何语言。本书作者是大名鼎鼎的Nicholas C. Zakas。他曾是Yahoo!的首席前端开发工程师,在完成了从一名独行侠到团队精英的蜕变后,他站在前端工程师的角度为我们提炼出许多的最佳编程实践,其中包括很多来自工业生产的最佳法则。应用这些技巧和技术,可以使你的团队编程从侠义的个人偏好的阴霾走出来,走向真正的高效和高水准。

本书由淘宝北京前端团队翻译,在翻译过程中,我们始终保持一种学习的心态,因为正像前面提到的,作者给出的很多经验正是我们手头工作中不在意却又至关重要的,这种学习心态也让我们在这次翻译过程收获颇丰。我们尽最大的努力,力求翻译后的表述在还原作者原意的同时又不失中文的流畅。但难免由于译者水平有限而有所纰漏,还请各位高手多多批评指正。

最后,我要感谢人民邮电出版社信息技术分社的陈冀康老师的信任和鼓励,宁愿让我们多花些时间来保证质量,同时感谢我的同事魏凡哲(陶清)、贺亮(完真)、杨翰文(地极)、王保平(玉伯)参与本书的试读和审校。如果要提交本书的勘误和建议,请在本书的介绍页面留言(http://jayli.github.com/maintainable.javascript)。

李晶(拔赤),郭凯(流火),张散集(一舟)

2013年1月


李晶,花名拔赤,淘宝前端工程师,具有多年前端开发经验,在团队协作、组件开发、移动Web App等方面有深入研究,曾经参与淘宝首页、KISSY等项目开发。他翻译过《JavaScript Web富应用开发》、《JavaScript权威指南(第六版)》、《What is Node?》等书籍,热爱分享,喜欢折腾。微博http://weibo.com/jayli

郭凯,花名流火,淘宝前端工程师,喜欢登山,也喜欢夜深人静时一人静静地写代码,折腾过许多小站均未能持久,有In、Juicer等开源项目。钟爱JavaScript,也喜欢Python,自嘲所学杂而不精。博客http://benben.cc

张散集,花名一舟,淘宝前端工程师。他主要从事技术管理,负责淘宝网(北京)的新业务技术和前端团队,热爱前端新技术的推广与应用。翻译作品有《JavaScript Web富应用开发》和《JavaScript权威指南(第六版)》。


这本书的封面动物是希腊陆龟(Greek tortoise)。

希腊陆龟(欧洲陆龟)也被称为股刺陆龟,它有至少20种已知的亚物种。它体型大,重量大,颜色深。它们的栖息地为北非、南欧和东南亚,这些地方比较炎热干旱,但也偶有生活于山区的干草地和海岸沙丘之中。它们基因的多样性导致它们很难被归类,而且通常会发生异种交配。不同种类的乌龟之间进行交配,会导致它们的后代在体型和颜色上差别甚大。正因为此,识别乌龟种类的最好方法就是搞清楚它们是来自哪里。

希腊陆龟的体型尺寸从8英寸到12英寸不等。它们大腿上的“鳞刺”和两种小结核有关系,这种小结核生长于尾部的两侧,通常也会在背部甲壳的长方形的“块”上见到。通常我们会从它们粗壮的前腿、斑点脊椎和肋板等特征来识别它们,它们的特点还包括巨大的背部斑点,它的甲壳直到尾部都是不可分的。

希腊陆龟在冬眠醒来之后会立即开始本能的交配。产卵之前会有一到两周的时间迁移它们的栖息地,它们通过挖、尝、闻泥土来寻找理想的产卵地点。在产卵之前一两天的时间里,雌性希腊陆龟会变得很有攻击性,以保证它的领地不会被袭扰,从而保证卵的安全。它们的平均寿命在50年左右。


由于前端工程师的成长道路自成轨迹,这让(Web前端技术)这个专业看起来是如此的与众不同,甚至诸如Yahoo!这种大公司里的很多前端工程师也多是靠自身野蛮生长,我行我素地写着各种hack![1]。或许你曾经也是一名小公司里的“页面仔”(the web guy),那时的你几乎什么都干。当大公司开始利用这些之前未被发掘的资源时[2],团队协作的环境短时间内吸纳了很多“hacker”[3],这时这些hacker碰到了各种条条框框。单兵作战再也没了优势,所有那些自学成才和以自我为中心的人都不得不开始思考如何才能在团队环境中生存下来。

我是在20世纪90年代末开始学习JavaScript的:靠的是自学。因为当时的JavaScript还很新潮,学习资料很少。和其他很多开发者一样,我通过不断钻研IE和Netscape Navigator来自学。我做各种尝试、梳理我所掌握的知识,我一遍遍反复试验,直到搞清楚其工作原理。幸运的是,这份好奇和勤勉为我带来了第一份工作。

在我职业生涯的第一个五年中,我是一个“脚本仔”。在我最早呆过的两家公司里,没有人比我更了解JavaScript和Web开发。所有问题,不管是非常简单的还是非常复杂的,我都能搞定。我刚刚从学校毕业,是一名初出茅庐的小伙子,却有着一种危机感。因为我找不出能和我产生思想碰撞的人,并且在我遇到问题时也没有人能为我解难。我力争做到最好,因为我知道我是唯一能做到这样的人。

在这五年之中,我不断磨炼我的技能。我让我的做事方式和工作流程更为合理。我不用担心其他人去研究我的代码,因为别人都没有能力给我做代码评审(review)或为我的代码提交bug修复。我是一个纯粹意义上的hacker:我行我素地写着代码,而且不用担心它会被修改。

在我职业生涯的第六年,我换了工作,加入了一个团队,在这个团队中,每个人都会为项目的各个方面贡献代码。我的主要精力不再是JavaScript和Web开发,而是大部分时间在写后端代码和SQL。同时,传统的后端工程师却开始被迫写前端(页面)代码。这种体验真正让我大开眼界:我之前写代码的方式和团队其他人实在是格格不入,这是一个大问题。

我很快意识到要想更高效地参与团队开发,我写代码的方式必须和团队保持一致。后台代码和SQL对我来说有点陌生,因此我采纳了身边一些有头脑的人写代码的模式。与此同时,我开始与其他工程师讨论我们应当采纳何种HTML、CSS和JavaScript的编程模式。我甚至在编译阶段加入了JavaScirpt语法检查来强制推行标准——这也是我第一次在公司做 Web 代码的检查。不久以后,这个团队就像上了润滑油的机器一样高效。

我在2006年加入了Yahoo!。我一到Yahoo!就发现几乎所有工作都和相互协作有关。整个团队就像把不同的动物纠集在一起。我所加入的第一个团队是My Yahoo!团队,这个团队非常庞大,比我之前工作过的任何团队都要大。团队里也有很多成型的编程指南,我有很多需要学习的东西。新技术、新流程以及新工具一股脑地展现在我面前。我花了大量的时间来学习这些新环境,吸收新知识,而这些知识是我从大学里接触不到的。在这里我完全沉浸在知识的海洋里,感觉自己像被重塑了一般。

几个月后,我开始遇到一些问题。我所习惯的开发流程并不是总能好好工作。有不少人以不同的方式来做事,这导致了很多bug。我的主管发现了这一苗头,一天他把我叫到一边,告诉我他希望我来负责梳理我们的开发。现在回想起他的话依然是那么鼓舞人心:“你写的代码都可以运行得很好,很少出bug。我希望其他人都像你一样写代码。”就这样,我打算着手为My Yahoo!前端开发团队注入一些结构和规范。

我为My Yahoo!团队做的工作非常成功,最终我被遴选为2008年Yahoo!新版首页的首席前端工程师。这次委任让我有机会将代码组织和测试高质量代码的技巧,应用于一个拥有20名前端工程师的团队,让这20名工程师只开发一种(风格的)代码。通过几个月的学习和调整,这个团队的生产效率和产品质量达到了一个很高的水平,让很多人叹为观止。不管是谁写的代码,它们都看起来像出自一个人之手,此外,多数工程师都可以快速地接手别人的工作来修复bug或开发新功能。在此期间,作为一个工程师团队所产出的业绩,在过去的几年里一直是我职业生涯的最大亮点。

我在Yahoo!的这段时间都是在参与大型的团队开发,我将在此期间积累的所有经验和方法整理出来成书于此。所讨论的主题也体现了我的这种蜕变,我从一名我行我素的独行侠,蜕变为一名软件工程师,一名团队协作者,我放弃了我身上固有的侠气和个性,来让整个团队以更高的水准运作。这正是这本书的内容:如何站在团队的角度去写JavaScript代码。

开发者最不容易理解的事情是,我们为何需要花这么多时间来“维护”代码。很少有人会打开一个文本编辑器从头开始写代码。大多数时间里,你面对的都是已经写好的代码。以一种可维护的方式来写代码,可以让你和协作者很容易就知道上段代码写到什么地方什么程度。正如我在Yahoo!时常说的:“当你开始工作时,你不是在给你自己写代码,而是为后来人写代码。”

本书收集了很多关于JavaScript编程规范的讨论。“Java语言编码规范”(Code Conventions for the Java Programming Language)[4]是当前最流行的关于编码规范的文档之一,它指出编码规范如此重要的几个原因。

这些原因在今天看来依然适用。本书所讨论的编程规范也是着眼于帮助你和你的团队写出高效的JavaScript代码。

因为你正在阅读这本书,你需要对本书提到的一些建议保持开放的心态。要知道这里说的很多技术,真正的目标是解决多人开发的环境中很多工程师如何书写统一风格的代码的问题。作为团队的一员,意味着做出这种决策不仅对个人有好处,对于团队也同样有好处。对于个人来说,需要在一定程度上牺牲个人偏好、个人观点甚至个人英雄主义。你所收获的是一个能做大事的高效的团队,我希望这本书可以帮助你做到这一点。

[1]  译注:hack是指手法肮脏但实现起来很简单且行之有效的解决方案,这类解决方案往往目光短浅,只解决眼前问题。

[2]  译注:大公司在业务扩张的时候会伴随大量的招聘,这时会有不少小公司的前端工程师被招募进来,文中提到的“资源”即是指这些人。

[3]  译注:hacker 有“黑客”的意思,偏褒义,但这里的意思是指那些不守规矩,我行我素的前端开发工程师。

[4]  译注:“Java语言编码规范”中译文地址:http://huihoo.org/code/java_code_conventions.html,by moyingzz@etang.com。


这个图标表示一种提示、建议或一般性的提醒。

这个图标表示一种警告。

本书是为了帮助你完成你的工作。通常来讲,你可以任意使用本书中的程序和文档。你不需要在这之前联系我们获得使用许可,但若复制程序的关键部分除外。比如,你的程序使用了本书中的多段代码,这不需要获取我们的许可。出售或者散布O'Reilly的书籍的示例代码光盘则需要首先获得授权。通过引用本书的内容或者代码来回答问题不需要预先获得许可,但将本书的示例代码的关键部分合并入你的产品或者文档则需要获得我们的许可。

我们不要求你一定注明本书的出处。一个完整的出处说明应当包括标题、作者、出版社和ISBN。比如:Maintainable JavaScript,作者Nicholas Zakas(O'Reilly)。Copyright 2012 Nicholas Zakas,978-1-449-32768-2。

如果你发现有人在我们的授权范围之外使用了示例代码,请联系我们permissions@
oreilly.com。

对于本书的评论或问题请联系出版商:

美国:

O'Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

中国:

北京市西城区西直门南大街2号成铭大厦C座807室(100035)

奥莱利技术咨询(北京)有限公司

申请会员或订阅图书目录,请发送邮件至:

info@oreilly.com

技术问题或评论本书,请发送邮件至:

bookquestions@oreilly.com

本书网页列出了勘误表、实例、其他附加信息以及后续版本计划。

http://shop.oreilly.com/product/06369200/024/.do

获取有关本书的更多信息、会议、资源中心和O'Reilly网站的相关信息,请查看我们的网站:

http://www.oreilly.com.cn

http://www.oreilly.com


“程序是写给人读的,只是偶尔让计算机执行一下。”

——Donald Knuth。[1]

当你刚刚组建一个团队时,团队中的每个人都各自有一套编程习惯。毕竟,每个成员都有着不同的背景。有些人可能来自某个“皮包公司”(one-man shop),身兼数职,在公司里什么事都做;还有些人会来自不同的团队,对某种特定的做事风格情有独钟(或恨之入骨)。每个人都觉得代码应当按照自己的想法来写,这些通常被归纳为个人编程嗜好。在这个过程中[2]应当尽早将确定统一的编程风格纳入议题。

我们会经常碰到这两个术语:“编程风格”(style guideline)和“编码规范”(code convention)。编程风格是编码规范的一种,用来规约单文件中代码的规划。编码规范还包含编程最佳实践、文件和目录的规划以及注释等方面。本书集中讨论JavaScript的编码规范。

提炼编程风格是一道工序,花再多的时间也不为过。毕竟每个人都有自己的想法,如果一天当中你有8小时是在写代码,那么你自然希望用一种舒服的方式来写代码。刚开始,团队成员对新的编程风格有点不适应,全靠强势的项目组长强制推行才得以持续。一旦风格确立后,这套编程风格就会促成团队成员高水准的协作,因为所有代码(的风格)看起来极为类似。

在团队开发中,所有的代码看起来风格一致是极其重要的,原因有以下几点。

任何开发者都不会在乎某个文件的作者是谁,也没有必要花费额外精力去理解代码逻辑并重新排版,因为所有代码排版格式看起来非常一致。我们打开一个文件时所干的第一件事,常常不是立即开始工作而是首先修复代码的缩进,当项目很庞大时,你会体会到统一的编程风格的确大幅度节省了时间成本。

我能很容易地识别出问题代码并发现错误。如果所有代码看起来很像,当你看到一段与众不同的代码时,很可能错误就产生在这段代码中。

毫无疑问,全球性的大公司都对外或对内发布过编程风格文档。

编程风格是个人的事情,只有放到团队开发中才能发挥作用。本书的这部分给出了JavaScript编码规范中值得关注(推荐)的方面。在某些场景中,很难说哪种编程风格好,哪种编程风格不好,因为有些编程风格只是某些人的偏好。本章不是向你灌输我个人的风格偏好,而是提炼出了编程风格应当遵循的重要的通用准则。本书附录A中给出了我个人的JavaScript编程风格。

开发编码指南是一件非常困难的事情——执行是另外一回事。在团队中通过讨论达成一致和进行代码评审(code review)时,每个人都很关注编码风格,但在平时大家却常常将这些抛在脑后。工具可以对每个人实时跟踪。这里有两个用来检查编程风格的工具,这两个工具非常有用:JSLint和JSHint。

JSLint是由Douglas Crockford创建的。这是一个通用的JavaScript代码质量检查工具。最开始,JSLint只是一个简单的查找不符合JavaScript模式的、错误的小工具。经过数年的进化,JSLint已经成为一个有用的工具,不仅仅可以找出代码中潜在的错误,而且能针对你的代码给出编码风格上的警告。

Crockford将他对JavaScript风格的观点分成了三个不同的部分。

JSLint直接吸纳了很多Crockford所提炼的编程风格,而且很多时候我们无法关闭JSLint中检查编程风格的功能。所以JSLint是一个非常棒的工具,当然前提是你认可Crockford关于编程风格的观点。

JSHint是JSLint的一个分支项目,由Anton Kovalyov创建并维护。JSHint的目标是提供更加个性化的JavaScript代码质量和编程风格检查的工具。比如,当出现语法错误的时候,JSHint几乎可以关掉所有编程风格检查,这样你可以完全自定义消息提示。Kovalyov非常鼓励大家通过GitHub(http://github.com)上的源代码库参与JSHint项目并为之贡献代码。

你可以将这些工具中的一种集成到打包过程中,通过这种方式推行编码规范是一个不错的方法。这种方法同时可以监控你的JavaScript代码中潜在的错误。

[1] 译注:高德纳(Donald Ervin Knuth)是世界顶级计算机科学家之一,被公认为现代计算机科学的鼻祖,著有《计算机程序设计艺术》(The Art of Computer Programming)等经典著作,在不多的业余时间里,Knuth不仅写小说,还是一位音乐家、作曲家、管风琴设计师。

[2] 译注:意指组建团队的过程。


编程风格指南的核心是基本的格式化规则(formatting rule)。这些规则直接决定了如何编写高水准的代码。与在学校学习写字时所用的方格纸类似,基本的格式化规则将指引开发者以特定的风格编写代码。这些规则通常包含一些你不太在意的有关语法的信息,但对于编写清晰连贯的代码段来说,每一条信息都是非常重要的。

关于JavaScript编码风格,我们首先要讨论的是(几乎所有的语言都是如此)如何处理缩进。对这个话题是可以争论上好几个小时的,缩进甚至关系到软件工程师的价值观。在确定编程风格之初应当首先确定缩进格式,这非常重要,以免工程师后续会陷入那个老生常谈的打开文件时二话不说先重排代码缩进的问题之中。来看一下这段代码(为了演示,这里故意修改了示例代码的缩进)。

if (wl && wl.length) {
               for (i = 0, l = wl.length; i < l; ++i) {
          p = wl[i];
          type = Y.Lang.type(r[p]);
          if (s.hasOwnProperty(p)) { if (merge && type == 'object') {

      Y.mix(r[p], s[p]);
} else if (ov || !(p in r)) {
                        r[p] = s[p];
               }
        }
    }
}

快速读懂这段代码并不容易。这里的缩进并不统一,一眼看去else是对应到第1行的if语句。但实际上这个else和代码第5行的if语句相对应。罪魁祸首是多位开发人员在同一段代码里应用了不同的缩进风格。这恰恰说明了统一缩进风格的重要性。如果有适当的缩进,这段代码将变得更加易读。

if (wl && wl.length) {
     for (i = 0, l = wl.length; i < l; ++i) {
          p = wl[i];
          type = Y.Lang.type(r[p]);
          if (s.hasOwnProperty(p)) {
               if (merge && type == 'object') {
                    Y.mix(r[p], s[p]);
               } else if (ov || !(p in r)) {
                    r[p] = s[p];
               }
          }
     }
}

坚持使用适度的缩进是万里长征的第一步——本章在下面将提到这种做法可以带来其他可维护性方面的提升。

对于大多数编程风格来说,代码到底应该如何缩进并没有统一的共识。有两种主张。

使用制表符进行缩进

每一个缩进层级都用单独的制表符(tab character)表示。所以一个缩进层级是一个制表符,两个缩进层级为两个制表符,以此类推。这种方法有两个主要的好处。第一,制表符和缩进层级之间是一对一的关系,这是符合逻辑的。第二,文本编辑器可以配置制表符的展现长度[1],因此那些想修改缩进尺寸的开发者可以通过配置文本编辑器来实现,想长即长,想短可短。使用制表符作缩进的主要缺点是,系统对制表符的解释不一致。你会发觉在某个系统中用一款编辑器打开文件时看到的缩进,和在另外一个系统中用相同的编辑器打开文件时看到的不一样。对于那些追求(代码展现)一致性的开发者来说,这会带来一些困惑。这些差异、争论会导致不同的开发者对同一段代码有不同的看法的,而这些正是团队开发需要规避的。

使用空格符进行缩进

每个缩进层级由多个空格字符组成。在这种观点中有三种具体的做法:2个空格表示一个缩进,4个空格表示一个缩进,以及8个空格表示一个缩进。这三种做法在其他很多编程语言中都能找到渊源。实际上,很多团队选择4个空格的缩进,对于那些习惯用2个空格缩进和用8个空格缩进的人来说,4个空格缩进是一种折中的选择。使用空格作缩进的好处是,在所有的系统和编辑器中,文件的展现格式不会有任何差异。可以在文本编辑器中配置敲击Tab键时插入几个空格。也就是说所有开发者都可以看到一模一样的代码呈现。使用空格缩进的缺点是,对于单个开发者来说,使用一个没有配置好的文本编辑器创建格式化的代码的方式非常原始。

尽管有人争辩说应当优先考虑使用一种缩进约定,但说到底这只是一个团队偏好的问题。这里我们给出一些各式各样的缩进风格作为参考。

我推荐使用4个空格字符为一个缩进层级。很多文本编辑器都默认将缩进设置为4个空格,你可以在编辑器中配置敲入Tab键时插入4个空格。使用2个空格的缩进时,代码的视觉展现并不是最优的,至少看起来是这样。

尽管是选择制表符还是选择空格做缩进只是一种个人偏好,但绝对不要将两者混用,这非常重要。这么做会导致文件的格式很糟糕,而且需要做不少清理工作,就像本节的第一段示例代码显示的那样。

有一件很有意思且很容易让人困惑的事情,那就是JavaScript的语句要么独占一行,要么以分号结尾。类似C的编程语言,诸如C++和Java,都采用这种行结束写法,即结尾使用分号。下面这两段示例代码都是合法的JavaScript。

// 合法的代码
var name = "Nicholas";
function sayName() {
     alert(name);
}
// 合法的代码,但不推荐这样写
var name = "Nicholas"
function sayName() {
     alert(name)
}

有赖于分析器的自动分号插入(Automatic Semicolon Insertion,ASI)机制,JavaScript代码省略分号也是可以正常工作的。ASI 会自动寻找代码中应当使用分号但实际没有分号的位置,并插入分号。大多数场景下ASI都会正确插入分号,不会产生错误。但ASI的分号插入规则非常复杂且很难记住,因此我推荐不要省略分号。看一下这段代码。

// 原始代码
function getData() {
     return
     {
         title: "Maintainable JavaScript",
         author: "Nicholas C. Zakas"
     }
}
// 分析器会将它理解成
function getData() {
     return;
     {
         title: "Maintainable JavaScript",
         author: "Nicholas C. Zakas"
     };
}

在这段代码中,函数getData()的本意是返回一个包含一些数据的对象。然而,return之后新起了一行,导致return后被插入了一个分号,这会导致函数返回值是undefined。可以通过将左花括号移至与return同一行的位置来修复这个问题。

// 这段代码工作正常,尽管没有用分号 
function getData() {
     return {
         title: "Maintainable JavaScript",
         author: "Nicholas C. Zakas"
     }
}

ASI在某些场景下是很管用的,特别是,有时候ASI可以帮助减少代码错误。当某个场景我们认为不需要插入分号而ASI认为需要插入时,常常会产生错误。我发现很多开发人员,尤其是新手们,更倾向于使用分号而不是省略它们。

Douglas Crockford针对JavaScript提炼出的编程规范(下文统称为Crockford的编程规范)推荐总是使用分号,同样,jQuery核心风格指南、Google的JavaScript风格指南以及Dojo编程风格指南都推荐不要省略分号。如果省略了分号,JSLint和JSHint默认都会有警告。

和缩进话题息息相关的是行的长度。如果一行代码太长,编辑窗口出现了横向滚动条,会让开发人员感觉很别扭。即便是在当今的宽屏显示器中,保持合适的代码行长度也会极大地提高工程师的生产力。很多语言的编程规范都提到一行代码最长不应当超过80 个字符。这个数值来源于很久之前文本编辑器的单行最多字符限制,即编辑器中单行最多只能显示80个字符,超过80个字符的行要么折行,要么被隐藏起来,这些都是我们所不希望的。相比20年前的编辑器,现在的文本编辑器更加精巧,但仍然有很多编辑器保留了单行80个字符的限制。此外关于行长度,还有一些常见的建议。

1.Java语言编程规范中规定源码里单行长度不超过80个字符,文档中代码单行长度不超过70个字符。

2.Android开发者编码风格指南规定单行代码长度不超过100个字符。

3.非官方的Ruby编程规范中规定单行代码长度不超过80个字符。

4.Python编程规范中规定单行代码长度不超过79个字符。

Java Script风格指南中很少提及行的长度,但Crockford的代码规范中指定一行的长度为80个字符。我也倾向于将行长度限定在80个字符。

当一行长度达到了单行最大字符数限制时,就需要手动将一行拆成两行。通常我们会在运算符后换行,下一行会增加两个层级的缩进。比如(假定缩进为4个字符)下面这样。

// 好的做法:在运算符后换行,第二行追加两个缩进
callAFunction(document, element, window, "some string value", true, 123,
          navigator);

// 不好的做法:第二行只有一个缩进
callAFunction(document, element, window, "some string value", true, 123,
     navigator);

// 不好的做法:在运算符之前换行了
callAFunction(document, element, window, "some string value", true, 123
        , navigator);

在这个例子中,逗号是一个运算符,应当作为前一行的行尾。这个换行位置非常重要,因为ASI机制会在某些场景下在行结束的位置插入分号。总是将一个运算符置于行尾,ASI就不会自作主张地插入分号,也就避免了错误的发生。

对于语句来说,同样也可以应用下面这种换行规则。

if (isLeapYear && isFebruary && day == 29 && itsYourBirthday &&
          noPlans) {

     waitAnotherFourYears();
}

在这段代码中,if条件语句被拆分成了两行,断行在&&运算符之后。需要注意的是,if语句的主体部分依然是一个缩进,这样更容易阅读。

这个规则有一个例外:当给变量赋值时,第二行的位置应当和赋值运算符的位置保持对齐。比如:

var result = something + anotherThing + yetAnotherThing + somethingElse +
               anotherSomethingElse;

这段代码里,变量anotherSomethingElse和首行的something保持左对齐,确保代码的可读性,并能一眼看清楚折行文本的上下文。

在编程规范中,空行是常常被忽略的一个方面。通常来讲,代码看起来应当像一系列可读的段落,而不是一大段揉在一起的连续文本。有时一段代码的语义和另一段代码不相关,这时就应该使用空行将它们分隔,确保语义有关联的代码展现在一起。我们可以为1.1节里的示例代码加入一些空行,以更好地提升代码的可读性,下面是最初的代码:

if (wl && wl.length) {
     for (i = 0, l = wl.length; i < l; ++i) {
          p = wl[i];
          type = Y.Lang.type(r[p]);
          if (s.hasOwnProperty(p)) {
               if (merge && type == 'object') {
                    Y.mix(r[p], s[p]);
               } else if (ov || !(p in r)) {
                    r[p] = s[p];
             }
         }
     }
}

给这段代码添加了几个空行之后,得到:

if (wl && wl.length) {

     for (i = 0, l = wl.length; i < l; ++i) {
          p = wl[i];
          type = Y.Lang.type(r[p]);

          if (s.hasOwnProperty(p)) {

               if (merge && type == 'object') {
                    Y.mix(r[p], s[p]);
               } else if (ov || !(p in r)) {
                    r[p] = s[p];
               }
          }
     }
}

这段示例代码中所展示的编程规范是在每个流控制语句之前(比如if和for语句)添加空行。这样做能使你更流畅地阅读这些语句。一般来讲,在下面这些场景中添加空行也是不错的主意。

但并没有一个编程规范对空行的使用给出任何具体建议,Crockford的编程规范也只提到要审慎地使用空行。

“计算机科学只存在两个难题:缓存失效和命名。”——Phil Karlton。

只要是写代码,都会涉及变量和函数,因此变量和函数命名对于增强代码可读性至关重要。JavaScript语言的核心ECMAScript,即是遵照了驼峰式大小写(Camel case)[2]命名法。驼峰式大小写(Camel Case)命名法是由小写字母开始的,后续每个单词首字母都大写,比如:

var thisIsMyName;
var anotherVariable;
var aVeryLongVariableName;

一般来讲,你应当遵循你使用的语言核心所采用的命名规范,因此大部分JavaScript程序员使用驼峰命名法来给变量和函数命名。Google的JavaScript风格指南、SproutCore编程风格指南以及Dojo编程风格指南在大部分场景中也都采用了小驼峰(Camel Case)命名。

尽管小驼峰(Camel Case)命名法是最常见的命名方法,但我们不排斥更多其他的命名风格。

在2000年左右,JavaScript中流行另外一种命名方法——匈牙利命名法。这种命名方法的特点是,名字之前冠以类型标识符前缀,比如sName表示字符串,iCount表示整数。这种风格已经是明日黄花风光不再了,当前主流的编程规范都不推荐这种命名法。

变量名应当总是遵守驼峰大小写命名法,并且命名前缀应当是名词。以名词作为前缀可以让变量和函数区分开来,因为函数名前缀应当是动词。这里有一些例子。

// 好的写法
var count = 10;
var myName = "Nicholas";
var found = true;

// 不好的写法:变量看起来像函数
var getCount = 10;
var isFound = true;

// 好的写法
function getName() {
     return myName;
}

// 不好的写法:函数看起来像变量
function theName() {
     return myName;
}

命名不仅是一门科学,更是一门技术,但通常来讲,命名长度应该尽可能短,并抓住要点。尽量在变量名中体现出值的数据类型。比如,命名count、length和size表明数据类型是数字,而命名name、title、和message表明数据类型是字符串。但用单个字符命名的变量诸如i、j、和k通常在循环中使用。使用这些能够体现出数据类型的命名,可以让你的代码容易被别人和自己读懂。

要避免使用没有意义的命名。那些诸如foo、bar和tmp之类的命名也应当避免,当然开发者的工具箱中还有很多这样可以随拿随用的名字,但不要让这些命名承载其他的附加含义。对于其他开发者来说,如果没有看过上下文,是无法理解这些变量的用处的。

对于函数和方法命名来说,第一个单词应该是动词,这里有一些使用动词常见的约定。

动  词

含  义

can

函数返回一个布尔值

has

函数返回一个布尔值

is

函数返回一个布尔值

get

函数返回一个非布尔值

set

函数用来保存一个值

以这些约定作为切入点可以让代码可读性更佳,这里有一些例子。

if (isEnabled()) {
     setName("Nicholas");
}

if (getName() === "Nicholas") {
     doSomething();
}

尽管这些函数命名细则并没有被归纳入当下流行的编程风格中,但在很多流行的库中,JavaScript开发者会发现存在不少这种“伪标准”(pseudostandard)。

jQuery显然并没有遵循这种函数命名约定,一部分原因在于jQuery中方法的使用方式,很多方法同时用作getter和setter。比如,$("body").attr("class")可以取到class属性的值,而$("body").attr("class","selected")可以给class属性赋值。尽管如此,我还是推荐使用动词作为函数名前缀。

在ECMAScript 6之前,JavaScript中并没有真正的常量的概念。然而,这并不能阻止开发者将变量用作常量。为了区分普通的变量(变量的值是可变的)和常量(常量的值初始化之后就不能变了),一种通用的命名约定应运而生。这个约定源自于C语言,它使用大写字母和下划线来命名,下划线用以分隔单词,比如:

var MAX_COUNT = 10;
var URL = "http://www.nczonline.net/";

需要注意的是,这里仅仅是应用了不同命名约定的变量而已,因此它们的值都是可以被修改的。使用这种不同的约定来定义普通的变量和常量,使两者非常易于区分。来看一下这段代码:

if (count < MAX_COUNT) {
     doSomething();
}

在这段代码中,一眼就能看出count是变量,其值是可变的,而MAX_COUNT表示常量,它的值不会被修改。这个约定为底层代码(underlying code)增加了另外一层语义。

Google的JavaScript风格指南、SproutCore编程风格指南以及Dojo编程风格指南中都提到,要使用这种习惯来命名常量。(Dojo编程风格指南中也允许使用大驼峰命名法大小写(Pascal Case)来命名常量,接下来会提到)。

在JavaScript中,构造函数只不过是前面冠以new运算符的函数,用来创建对象。语言本身已经包含了很多内置构造函数,比如Object和RegExp,同样开发者也可以创建自己的构造函数来生成新类型。正如其他的命名约定一样,构造函数的命名风格也和本地语言(Native Language)保持一致,因此构造函数的命名遵照大驼峰命名法(Pascal Case)。

Pascal Case和Camel Case都表示“驼峰大小写”,二者的区别在于Pascal Case以大写字母开始。因此anotherName可以替换成AnotherName。这样做可以将构造函数从变量和普通函数中区分出来。构造函数的命名也常常是名词,因为它们是用来创建某个类型的实例的。这里有一些例子:

// 好的做法
function Person(name) {
     this.name = name;
}

Person.prototype.sayName = function() {
     alert(this.name);
};

var me = new Person("Nicholas");

遵守这条约定同样可以帮助我们快速定位问题,这接下来会提到。你知道在以大驼峰命名法(Pascal case)命名的函数如果是名词的话,前面一定会有new运算符。看一下这段代码:

var me = Person("Nicholas");
var you = getPerson("Michael");

这段代码中,根据上文提到的命名约定,我们一眼就可以看出第一行出了问题,但第二行看起来还ok。

Crockford的编程规范、Google的JavaScript风格指南以及Dojo编程风格指南都推荐这种实践。如果构造函数的首字母不是大写,或者构造函数之前没有new运算符,JSLint都会给出警告。而在JSHint中,只有你开启了一个特殊的newcap选项后,才会对首字母不是大写的构造函数给出警告。

JavaScript中包含一些类型的原始值:字符串、数字、布尔值、null和undefined。同样也包含对象直接量和数组直接量。这其中,只有布尔值是自解释(self-explanatory)的,其他的类型或多或少都需要思考一下它们如何才能更精确地表示出来。

在JavaScript中,字符串是独一无二的。字符串可以用双引号括起来,也可以用单引号括起来。比如:

// 合法的JavaScript代码
var name = "Nicholas says, \"Hi.\"";

// 也是合法的JavaScript代码
var name = 'Nicholas says, "Hi"';

和Java、PHP这些语言不同,使用单引号括起字符串和双引号括起字符串在功能上并无不同。除了内部出现字符串界定符(string delimiter)时需要转义之外,两种做法在功效上完全一致。因此在这段示例代码中,在使用双引号括起来的字符串里需要对双引号进行转义,而在使用单引号括起来的字符串里则不必如此。你需要关心的是,你的代码应当从头到尾只保持一种风格。

Crockford的编程规范和jQuery核心风格指南都使用双引号来括住字符串。Google的JavaScript风格指南使用单引号括住字符串。我倾向于使用双引号,因为我经常在Java和JavaScript之间来回切换。由于Java只使用双引号括住字符串,我发现如果在JavaScript中也使用这个约定,我会很容易在上下文之间相互切换。这类问题应当在制定规范之初就考虑清楚:这样可以最大程度地减轻工程师的开发负担。

关于字符串还有另外一个问题需要注意,即创建多行字符串。这个特性并非来自JavaScript语言本身,却在几乎所有的(JavaScript)引擎中正常工作。

// 不好的写法
var longString = "Here's the story, of a man \
named Brady.";

尽管从技术上讲这种写法是非法的JavaScript语法,但的确能在代码中创建多行字符串。通常不推荐使用这种写法,因为它是一种奇技淫巧而非语言特性,并且在Google的JavaScript风格指南中是明确禁止的。多行字符串的一种替代写法是,使用字符串连接符(+)将字符串分成多份。

// Good
var longString = "Here's the story, of a man " +
                    "named Brady.";

在JavaScript中的数字类型只有一种,因为所有数字形式——整数和浮点数——都存储为相同的数据类型。还有一些其他的数字直接量格式来表示不同的数据格式。其中大部分写法都很好用,但也有一些写法有问题。

// 整数
var count = 10;

// 小数
var price = 10.0;
var price = 10.00;

// 不推荐的小数写法:没有小数部分
var price = 10.;

// 不推荐的小数写法:没有整数部分
var price = .1;

// 不推荐的写法:八进制写法已经被弃用了
var num = 010;

// 十六进制写法
var num = 0xA2;

// 科学计数法
var num = 1e23;

前两种有问题的写法分别是省略了小数部分,比如10.,和省略了整数部分,比如.1。每种写法都有同一个问题:很难搞清楚被省略小数点之前或之后的部分是不小心丢掉了还是刻意为之。很可能是开发者不小心漏掉了。因此为了避免歧义,请不要省略小数点之前或之后的数字。Dojo编程风格指南明确禁止这两种写法。JSLint和JSHint对这两种写法都会给出警告。

最后一个有问题的写法是八进制数字写法。长久以来,JavaScript支持八进制数字写法是很多错误和歧义的根源。数字直接量010不是表示10,而是表示八进制中的8。大多数开发者对八进制格式并不熟悉,也很少用到,所以最好的做法是在代码中禁止八进制直接量。尽管在所有流行的编程规范中没有关于此的规定,但在JSlint和JSHint中都会对八进制直接量给出警告。

null是一个特殊值,但我们常常误解它,将它和undefined搞混。在下列场景中应当使用null。

还有下面一些场景不应当使用null。

这里有一些示例代码。

// 好的用法
var person = null;

// 好的用法
function getPerson() {
     if (condition) {
          return new Person("Nicholas");
     } else {
          return null;
     }
}

// 好的用法
var person = getPerson();
if (person !== null) {
     doSomething();
}

// 不好的写法:用来和未初始化的变量比较
var person;
if (person != null) {
     doSomething();
}

// 不好的写法:检测是否传入了参数
function doSomething(arg1, arg2, arg3, arg4) {
     if (arg4 != null) {
          doSomethingElse();
    }
}

理解null最好的方式是将它当做对象的占位符(placeholder)。这个规则在所有的主流编程规范中都没有提及,但对于全局可维护性来说至关重要。

关于null的陷阱会在第8章有更进一步的讨论。

undefined是一个特殊值,我们常常将它和null搞混。其中一个让人颇感困惑之处在于null == undefined结果是true。然而,这两个值的用途却各不相同。那些没有被初始化的变量都有一个初始值,即undefined,表示这个变量等待被赋值。比如:

// 不好的写法
var person;
console.log(person === undefined); //true

尽管这段代码能正常工作,但我建议避免在代码中使用undefined。这个值常常和返回“undefined”的typeof运算符混淆。事实上,typeof的行为也很让人费解,因为不管是值是undefined的变量还是未声明的变量,typeof运算结果都是“undefined”。比如:

//foo未被声明
var person;
console.log(typeof person);                 //"undefined"
console.log(typeof foo);                     //"undefined"

在这段代码中,person和foo都会导致typeof返回“undefined”,哪怕person和foo在其他场景中的行为有天壤之别(在语句中使用foo会报错,而使用person则不会报错)。

通过禁止使用特殊值undefined,可以有效地确保只在一种情况下typeof才会返回“undefined”:当变量未声明时。如果你使用了一个可能(或可能不会)赋值为一个对象的变量时,则将其赋值为null。

// 好的做法
var person = null;
console.log(person === null); //true

将变量初始值赋值为 null 表明了这个变量的意图,它最终很可能赋值为对象。typeof运算符运算null的类型时返回“object”,这样就可以和undefined区分开了。

创建对象最流行的一种做法是使用对象直接量,在直接量中直接写出所有属性,这种方式可以取代先显式地创建Object的实例然后添加属性的这种做法。比如,我们很少见到下面这种写法。

// 不好的写法
var book = new Object();
book.title = "Maintainable JavaScript";
book.author = "Nicholas C. Zakas";

对象直接量允许你将所有的属性都括在一对花括号内。直接量可以高效地完成非直接量写法相同的任务,非直接量写法语法看起来更复杂。

当定义对象直接量时,常常在第一行包含左花括号,每一个属性的名值对都独占一行,并保持一个缩进,最后右花括号也独占一行。比如:

// 好的写法
var book = {
     title: "Maintainable JavaScript",
     author: "Nicholas C. Zakas"
};

这种写法在开源JavaScript代码中能经常看到。尽管没有归纳入文档中,Google的JavaScript风格指南非常推荐使用这种写法。Crockford的编程规范也推荐使用直接量代替Object构造函数,但并没有给出具体的书写格式。

和对象直接量类似,数组直接量是JavaScript中定义数组最简洁的一种方式。不赞成显式地使用Array构造函数来创建数组,比如:

// 不好的写法
var colors = new Array("red", "green", "blue");
var numbers = new Array(1, 2, 3, 4);

可以使用两个方括号将数组初始元素括起来,来替代使用Array构造函数的方式来创建数组。

// 好的做法
var colors = [ "red", "green", "blue" ];
var numbers = [ 1, 2, 3, 4 ];

在JavaScript中,这种模式非常常见。Google的JavaScript风格指南和Crockford的编程规范都推荐这样做。

[1] 译注:通常一个制表符长度相当于4个字符。

[2] 译注:Camel Case和Pascal Case都翻译成“驼峰式大小写”,但两者含义有所不同,Camel Case包括“小驼峰式”和“大驼峰式”,在本章中,Camel Case被作者用来特指“小驼峰式大小写”(即首字母小写)命名法,Pascal Case则特指“大驼峰式大小写”(即首字母大写)命名法,请读者留意。


注释是代码中最常见的组成部分。它们是另一种形式的文档,也是程序员最后才舍得花时间去写的。但是,对于代码的总体可维护性而言,注释是非常重要的一环。打开一个没有任何注释的文件就好像趣味冒险,但如果给你的时间有限,这项任务就变成了折磨。适度的添加注释可以解释说明代码的来龙去脉,其他开发者就可以不用从头开始读代码,而是直接去读代码的任意部分。编程风格通常不会包含对注释的风格约定,但我认为从注释的作用即可看出它们的重要性不容忽视。

JavaScript支持两种不同类型的注释:单行注释和多行注释。

单行注释以两个斜线开始,以行尾结束。

// 这是一句单行注释

很多人喜欢在双斜线后敲入一个空格,用来让注释文本有一定的偏移。单行注释有三种使用方法。

单行注释不应当以连续多行注释的形式出现,除非你注释掉一大段代码。只有当需要注释一段很长的文本时才使用多行注释。

这里有一些示例代码。

// 好的写法
if (condition) {

     // 如果代码执行到这里,则表明通过了所有安全性检查
     allowed();
}

// 不好的写法:注释之前没有空行
if (condition) {
     // 如果代码执行到这里,则表明通过了所有安全性检查
     allowed();
}

// 不好的写法:错误的缩进
if (condition) {

// 如果代码执行到这里,则表明通过了所有安全性检查
     allowed();
}

// 好的写法
var result = something + somethingElse; // somethingElse不应当取值为null

// 不好的写法:代码和注释之间没有间隔
var result = something + somethingElse;// somethingElse不应当取值为null

// 好的写法
// if (condition) {
//       doSomething();
//       thenDoSomethingElse();
// }

// 不好的写法:这里应当用多行注释
// 接下来的这段代码非常难,那么,让我详细解释一下
// 这段代码的作用是首先判断条件是否为真
// 只有为真时才会执行。这里的条件是通过
// 多个函数计算出来的,在整个会话生命周期内
// 这个值是可以被修改的
if (condition) {
     // 如果代码执行到这里,则表明通过了所有安全性检查
     allowed();
}

多行注释可以包裹跨行文本。它以/*开始,以*/结束。多行注释不仅仅可以用来包裹跨行文本,这取决于你。下面这些都是合法的注释。

/* 我的注释 */
/* 另一段注释
这段注释包含两行 */
/*
又是一段注释
这段注释同样包含两行
*/

尽管从技术的角度看,这些注释都是合法的,但我比较青睐Java风格的多行注释。Java风格的注释至少包含三行:第一行是/*,第二行是以*开始且和上一行的*保持左对齐,最后一行是*/。这种注释看起来像下面这样。

/*
 * 另一段注释
 * 这段注释包含两行文本
 */

通过在注释块左侧注上星号,会让注释更加清晰。有一些IDE(比如NetBean和Eclipse)会为你自动插入这些星号。

多行注释总是会出现在将要描述的代码段之前,注释和代码之间没有空行间隔。和单行注释一样,多行注释之前应当有一个空行,且缩进层级和其描述的代码保持一致。来看下面这段例子。

// 好的写法
if (condition) {

     /*
      * 如果代码执行到这里
      * 说明通过了所有的安全性检测
      */
     allowed();
}

// 不好的写法:注释之前无空行
if (condition) {
     /*
      * 如果代码执行到这里
      * 说明通过了所有的安全性检测
      */
     allowed();
}

// 不好的写法:星号后没有空格
if (condition) {
     /*
      *如果代码执行到这里
      *说明通过了所有的安全性检测
      */
     allowed();
} 

// 不好的写法:错误的缩进
if (condition) {

/*
 * 如果代码执行到这里
 * 说明通过了所有的安全性检测
 */
     allowed();
}

// 不好的写法:代码尾部注释不要用多行注释格式
var result = something + somethingElse; /*somethingElse 不应当取值为null*/

何时添加注释是程序员经常争论的一个话题。一种通行的指导原则是,当代码不够清晰时添加注释,而当代码很明了时不应当添加注释。比如这个例子中,注释是画蛇添足。

// 不好的写法

// 初始化count
var count = 10;

因为代码中初始化count的操作是显而易见的。注释并没有提供其他有价值的信息。换个角度讲,如果这个值10具有一些特殊的含义,而且无法直接从代码中看出来,这时就有必要添加注释了。

// 好的写法

// 改变这个值可能会让它变成青蛙
var count = 10;

当然不可能因为修改了count的值它就变成了青蛙,但这的确是一个好的注释写法的例子,因为注释中给出了必要的信息,如果没有注释,你不可能获得这些信息。想象一下如果你修改了count的值它真的变成了青蛙,实在是让人困惑不解,一切都源于你没有写这句注释。

因此,添加注释的一般原则是,在需要让代码变得更清晰时添加注释。

难于理解的代码通常都应当加注释。根据代码的用途,你可以用单行注释、多行注释,或是混用这两种注释。关键是让其他人更容易读懂这段代码。比如,这段示例代码摘自YUI类库中的Y.mix()方法。

// 好的写法

if (mode) {

     /*
      * 当mode为2时(原型到原型,对象到对象),这里只递归执行一次
      * 用来执行原型到原型的合并操作。对象到对象的合并操作
      * 将会被挂起,在合适的时机执行
      */
     if (mode === 2) {
          Y.mix(receiver.prototype, supplier.prototype, overwrite,
                    whitelist, 0, merge);
    }

     /*
      * 根据指定的模式类型,我们可能会从源对象拷贝至原型中,
      * 或是从原型拷贝至接收对象中
      */
     from = mode === 1 || mode === 3 ? supplier.prototype : supplier;
     to   = mode === 1 || mode === 4 ? receiver.prototype : receiver;

     /*
      * 如果supplier或receiver不含有原型属性时,
      * 则逻辑结束,并返回undefined。如果有原型属性,
      * 则逻辑结束并返回receiver
      */
     if (!from || !to) {
          return receiver;
    }
} else {
     from = supplier;
     to   = receiver;
}

Y.mix()方法使用常量来决定如何处理。mode参数就表示这些常量其中之一,但仅仅通过这些数值无法理解它们各自代表的含义。这里的注释非常棒,因为它及时地解释了这里复杂的决策逻辑。

另一个适合添加注释的好时机是当代码看上去有错误时。在团队开发中,总是会有一些好心的开发者在编辑代码时发现他人的代码错误,就立即将它修复。有时这段代码并不是错误的源头,所以“修复”这个错误往往会制造其他错误,因此本次修改应当是可追踪的。当你写的代码有可能会被别的开发者认为有错误时,则需要添加注释。这里是另一段来自YUI源码的例子。

while (element &&(element = element[axis])) { // 提示: 赋值操作
     if ( (all || element[TAG_NAME]) &&
          (!fn || fn(element)) ) {
                 return element;
     }
}

在这个例子中,开发者在while循环控制条件中使用了一个赋值运算符。这不是一种标准用法,并常常被检测工具认为是有问题的。如果你对这段代码不熟悉,读到这段没有注释的代码时,很可能误以为这是一个错误,猜想作者的本意是使用比较运算符==而不是赋值运算符=。这行末尾的注释说明作者是有意为之,即赋值而非比较。这样,其他开发者在读到这段代码时就不会将它“修复”。

JavaScript程序员常常会编写一些低效的、不雅的、彻头彻尾的肮脏代码,用来让低级浏览器正常工作。实际上这种情形是一种特殊的“可能被误认为错误的代码”:这种不明显的做浏览器特性Hack的代码可能隐含一些错误。这里有个例子,是摘自YUI类库的Y.DOM.contains()方法。

var ret = false;

if ( !needle || !element || !needle[NODE_TYPE] || !element[NODE_TYPE]) {
    ret = false;
} else if (element[CONTAINS]) {
    // 如果needle不是ELEMENT_NODE时,IE和Safari下会有错误
    if (Y.UA.opera || needle[NODE_TYPE] === 1) {
         ret = element[CONTAINS](needle);
    } else {
         ret = Y_DOM._bruteContains(element, needle);
    }
} else if (element[COMPARE_DOCUMENT_POSITION]) { // gecko
     if (element === needle || !!(element[COMPARE_DOCUMENT_POSITION](needle) & 16)) {
         ret = true;
     }
}

return ret;

这段代码的第6行包含一条重要的注释。尽管IE和Safari中都有内置方法contains(),但如果needle不是一个元素时,这个方法会报错。所以只有当浏览器是Opera时才能用这个方法,其他浏览器中needle必须是一个元素(nodeType是1)。这里关于浏览器的说明同样解释了为什么需要一个if语句,这个注释不仅确保将来不会被其他人误改动,而且在代码编写者回过头阅读自己的这段代码时,也会适时地针对新版本的IE和Safari的兼容情况做出调整。

从技术的角度讲,文档注释并不是JavaScript的组成部分,但它们是一种普遍的实践。文档注释有很多种格式,但最流行的一种格式来自于JavaDoc文档格式:多行注释以单斜线加双星号(/**)开始,接下来是描述信息,其中使用@符号来表示一个或多个属性。来看一段来自YUI的源码的例子。

/**

返回一个对象,这个对象包含被提供对象的所有属性。

后一个对象的属性会覆盖前一个对象的属性。

传入一个单独的对象,会创建一个它的浅拷贝(shallow copy)。

如果需要深拷贝(deep copy),请使用'clone()'
@method merge
@param {Object} 被合并的一个或多个对象
@return {Object} 一个新的合并后的对象
**/
Y.merge = function () {
     var args    = arguments,
          i       = 0,
          len     = args.length,
          result  = {};

     for (; i < len; ++i) {
          Y.mix(result, args[i], true);
     }

     return result;
};

YUI类库使用它自己的一个名叫YUIDoc的工具来根据这些注释生成文档。但是,它的格式几乎和JSDoc Toolkit(类库无关的)一模一样,在开源项目中JSDoc Toolkit的应用非常广泛,包括Google内部的很多开源项目。YUIDoc和JSDoc Toolkit之间的重要区别是,YUIDoc同时支持文档注释中的HTML和Markdown格式,而JSDoc Toolkit只支持HTML。

这里强烈推荐你使用文档生成工具来为你的JavaScript生成文档。JavaScript代码注释必须符合你所用的工具支持的格式,但很多文档生成工具都支持JavaDoc风格的文档注释。当使用文档注释时,你应当确保对如下内容添加注释。

所有的方法

应当对方法、期望的参数和可能的返回值添加注释描述。

所有的构造函数

应当对自定义类型和期望的参数添加注释描述。

所有包含文档化方法的对象

如果一个对象包含一个或多个附带文档注释的方法,那么这个对象也应当适当地针对文档生成工具添加文档注释。

当然,注释的详细格式和用法最终还是由你所选择的文档生成工具决定的。


相关图书

深入浅出Spring Boot 3.x
深入浅出Spring Boot 3.x
JavaScript核心原理:规范、逻辑与设计
JavaScript核心原理:规范、逻辑与设计
JavaScript入门经典(第7版)
JavaScript入门经典(第7版)
JavaScript函数式编程指南
JavaScript函数式编程指南
PHP、MySQL和JavaScript入门经典(第6版)
PHP、MySQL和JavaScript入门经典(第6版)
JavaScript学习指南(第3版)
JavaScript学习指南(第3版)

相关文章

相关课程