JavaScript 开发最佳实践

写一篇关于最佳实践的文章是十分困难的事情。你们将要读到的内容是显而易见,但是明智的做法。

然而,多年来在网上浏览并查看其他开发者提交的代码的经历告诉我,想要在网络的编程环境中找到一定的常识是几乎不可能的事,并且一旦你身处于一个项目中时,随着 deadline 的步步逼近,“做明智且有逻辑的事”就会在优先级列表中被推得越来越远。

所以,我决定创作这篇文章更简单地让你了解,什么是最佳实践,以及我这么多年积累的优秀建议。其中大部分都来之不易(通过实验等方法得到的)。记住下面给出的建议,让它们成为你大脑的一部分,这是一个快速有效的方法帮助你不用考虑太多就能接受这些建议。我相信你一定会发现与你的想法不一致的地方,这是好事 —— 你应该质疑读到的东西,然后努力找到更好的解决方法。不过,我发现遵循下面这些原则可以使我成为一名更有效率的开发者,也方便了其他开发者在我的工作基础上进行后续开发。

下面是本文的组织结构:

给变量和函数命名 ——变量名和函数名尽量简短且易读

一种很简单但又很可怕的情况是你经常会在 JavaScript 中碰到像 x1fe2 或者 xbqne 的变量名,或者 —— 另一种极端命名 —— 像incrementorForMainLoopWhichSpansFromTenToTwenty 或者createNewMemberIfAgeOverTwentyOneAndMoonIsFull 这样的变量名。

这种命名没有任何意义 —— 好的变量和函数命名应该易于理解,并告知它的作用 —— 不多也不少。还有一个需要避免的陷阱是在命名中将数值与功能结合。由于合法饮酒年龄因国家而异,isLegalDrinkingAge() 就比 isOverEighteen() 更加适合,除了饮酒外,还有其他事情也是一样需要考虑年龄限制的。

匈牙利命名法是一种值得采用的不错的命名方案(还有一些其他的命名方案可以考虑),优势在于你知道应该怎样去命名,而不仅仅知道它是什么名字。

举个例子,如果你有一个名为 familyName 的变量,它是一个字符串,那么根据 “匈牙利命名法” 你应该将其写为 sFamilyName。一个名为 member 的对象可被写为 oMember,一个名为 isLegal 的布尔对象可被写为 bIsLegal。在某些情况下,这种命名信息量很大,但在某些情况下又似乎额外开销太大,这取决于你是否选择使用它。

保持英文也是一个好主意。编程语言都使用英文,所以为何不把这个作为你代码逻辑中的一环呢。调试一段时间的韩语与斯洛文尼亚语代码后,我向你保证这对于一名非母语者来说的确不是一件有趣的事。

把你的代码当成叙事。如果你可以一行一行地阅读并理解它在讲述什么,说明你做的不错。如果你需要使用画板来理清逻辑流程,那么你的代码还需要再优化一下。如果你想要对比一个真实的世界,试着去阅读 Dostojewski(俄国作家)的作品吧 —— 看见写有 14 个俄罗斯名字的一页后,我完全迷茫了,其中有 4 个都是假名。不要写出那样的代码 —— 虽然那样看起来更像艺术而非产品,但并不是一件好事。

避免全局

全局变量和函数名是一个非常糟糕主意。因为页面中的每个 JavaScript 文件都在同一个范围内运行。如果你的代码中有全局变量或者函数,后面脚本中包含的相同变量和函数名将会覆盖你的全局变量或函数。

下面是几个避免使用全局变量的变通方法 —— 现在我们一个个展示它们。如果你有三个函数和一个变量,就像这样:

你可以使用一个对象字面量来保护它们不被重写:

这会起作用,但有一个缺点 —— 调用函数或改变变量值时,你都需要使用主对象的名字:init()myNameSpace.init()currentmyNameSpace.current 等等。这很讨厌并重复。

但我们可以简单地使用一个匿名函数包含所有事物并通过这种方式保护此域。同样意味着你不需将语法从 function name() 转换为 name:function()。这个特性被称为模块化开发:

继续,这样改进仍有问题。这些方法与变量对于外界来说都不再可用。如果你想让它们可用,需要在一个 return 代码块里包含你想要让其变成 public 的事物:

在链接两个方法和改变语法方面,这几乎把我们带回了起点。因此我更喜欢像下面一样(我赋予其“展示性模块化”称号):

代替返回属性和函数,我仅仅返回了指向它们的内容。这使得其他地方可以不通过 myNameSpace,更简单地调用函数以及访问变量。

这也意味着对于一个函数,如果你想要为内部链接赋予一个更长的描述性的名称,但同时为外部赋予一个更短的名称时,可以拥有一个公共别名:

现在调用 myNameSpace.set() 将会链接到 change() 方法。

如果你完全不需要外部使用你的变量或者函数,可以简单地使用另一对括号包围整个结构,执行的时候不声明任何名称。

这样就可以将所有东西装进一个整洁的小包裹里,外界无法访问,但里面却能够轻松地共享变量和函数。

坚持严格的代码规范

浏览器对于 JavaScript 语法是十分宽容的,但这不能成为你编写完全依赖浏览器运行的草率代码的理由。

让你的代码通过 JSLint(一个可以给予你代码中语法警告的详细报告及意义的 JavaScript 审查工具)运行是检查自己代码质量最简单的方法。人们已为各种编辑器编写了插件(比如 JS Tools for TextMate),可以在保存的时候自动检查你的代码。

JSLint 返回的结果可能会比较棘手,正如其开发者 Douglas Crockford 所说,这个工具会打击你的感受。然而当我安装 TextMate JS bundle 并开始遵循 JSLint 的规范编码,发现自己写出了比以前好出不少的代码。

清晰有理的代码意味着更少修复令人困惑的 bug,更简单地移交给其他开发者以及更棒的安全性。当你依赖于从各处窃取代码并使其工作时,很可能还有一处安全漏洞也使用了同样的代码。补充一句,当浏览器修复了自己不完善的地方,你的代码在其下个版本就会失效。

另外,有效的代码意味着可以通过脚本转换为其他格式,而 hacky code(丑陋的代码)却需要人为做那样的事情。

如果需要的话,尽可能多地添加注释,但也不要太多

注释是你传达给其他开发者的信息(和你自己,如果你在其他地方工作几个月后回来查看你的代码)。数年来人们一直激烈地争吵到底要不要使用注释,主要的争论点在于优秀的代码应该能自我解释。

在我看来,这场争论的辩解实在太主观了。你没办法期待每一名开发者都能靠着同样的解释去理解一些代码在干什么。

如果你正确地添加注释,它们不会影响到任何人。我们会在这篇文章的最后一点说明,但是如果最终用户们在代码的最后看见你的一堆注释,就不太好了。

另一个窍门是保持节制。如果你想要使用 /* */ 符号进行注释,那么就应该说明一件重要的事情。使用 // 的单行注释会造成一个问题,人们可能因为你没有剥离注释而看轻你的代码,一般来说,你并不需要那么多注释。

如果你想注释掉自己的一部分代码等待后面开发时使用或调试其他代码,下面有一种可以选择的技巧:

编写代码时在闭合的 */ 之前增加一个 //,这样你就可以通过在开始的 /* 前面简单地增加或删除一个 / 来注释或取消注释整个代码块。

就如上面所示的那样编写代码,在开始的 /* 前面增加一个 /,将会把多行注释转换为两个单行注释,“unhiding”(不再隐藏)之间的代码,使其被执行。移除这个 / 则会重新注释。

JavaDoc 规范中大量关于应用注释文档的内容十分有意义。你在编码的同时就在生成自己产品的全部文档。Yahoo User Interface library 的成功在一定程度上得归功于这点,其甚至可以成为一个工具帮助你为自己产品创建一份相同的文档。在你成为一名更有经验的 JavaScripter 之前都不太需要关心这个,我只是为了完整性提及一下 JavaDoc。

避免在 JavaScript 中混合其他技术

你可以在文档中同时使用 JavaScript 和 DOM 创造自己需要的任何事物,但这肯定不是完成同一件事情最有效率的方式。下面的代码给每个 class 为 “mandatory” 的输入框增加了一个红色 border,里面没有任何事物。

这样能起作用,然而也意味着如果之后需要对样式做一些改变,你得通过修改这里的 JavaScript 代码实现。更改越复杂,编辑这里的代码就变得越困难。另外,不是每一名 JavaScript 开发者都能够熟练使用 CSS 或者对其感兴趣,这意味着在达成结果之前,你得在 JavaScript 中反复做大量地修改。当这里有一个错误时,给该 element 增加一个名为 “error” 的 class,通过这样的行为你可以确保该样式信息处于更有效的 CSS 中:

如果你打算使用 CSS 修饰 document 层叠,将会增加大量效率。再举一个例子,如果你想要隐藏 document 中有同一个 class 的所有 DIV。你可以遍历所有 DIV,检查它们的 class,然后改变它们的样式集合。在较新的浏览器中,你也可以使用一个 CSS 选择器引擎修改同样的样式集合。但是最简单地方法是,使用 JavaScript 在 parent element 上设置一个 class,然后在 CSS 中使用 div.selectorclass{} 的语法遍历每行 element.triggerclass。把隐藏 DIV 的任务交给 CSS 设计师吧,他知道怎么做最好。

当有实用意义的时候,可以考虑使用简化符号

简化符号是一把双刃剑:一方面它可以使你的代码保持简短,另一方面对于其他接手你的代码的开发者而言,会使阅读变得困难,他们或许无法搞清楚这些简化符号干了什么。不过,下面我会列出几种可以使用(并且应该使用)简化符号的情况。

对象或许是你在 JavaScript 中最常用的事物。传统方法书写对象一般是这样做:

但这意味着你得为每个属性或函数重写一遍对象名,这可真是令人厌烦啊。我们可以取而代之,使用下面这种更有意义的结构,也被称为 object literal:

在 JavaScript 中数组是一个容易令人困惑的知识点。你会发现大量的脚本中都这样定义一个数组:

这里进行了大量无意义的重复操作;其实可以使用 [] 数组缩写符更加快速地创建一个数组:

你可以在一些教程里面发现这个术语 “associative array”(关联数组)。这其实是一个误称,带有内容声明而不是一个 index 的数组实际上就是对象,同样应该被定义。

条件选择句同样可以使用 “三元选择符” 缩短。例如下面的结构定义了一个值为 1 或者 -1 的变量,取决于另一个变量的值:

我们可以将其缩短为单独一行:

问号前面的内容都是判断条件,紧接其后的值是为 true 的情况,再后面的值是为 false 的情况。三元选择符可以嵌套,但为了保持可读性我都避免那样使用。

另一个 JavaScript 中常见的情形是当变量未定义时,为其提供一个预设值,就像这样:

此处的简短符号是 ||

这样就可以自动判断当 v 未定义时,为 x 赋值 10,就这么简单。

模块化 —— 每个任务对应一个函数

这是一个通用的编程最佳实践 —— 请确定你创建的函数一次只完成一个工作,这样其他开发者可以简单地调试与修改你的代码,而不需浏览所有代码才能弄清每一个代码块执行了什么功能。

同样也能应用于创建通用任务的辅助函数。如果你发现自己在不同的函数中做着相同的事情,那么最好创建一个更加通用的辅助函数,需要时重用其功能。

并且,一进一出比在函数内部修改代码更有意义。比如说你想编写一个创建新链接的辅助函数。可以这样做:

这能够好好工作,但你或许会发现自己又不得不增加不同的 attribute,取决于你要给哪种 element 增加它适用的链接。举个例子:

这会使该函数更加特殊,难以适用于不同情形。一个更加清晰地方式是返回这个链接,当需要的时候,在主函数中覆盖这些额外的情况。在这里将 addLink() 改为更加通用的 createLink()

通过使自己的所有函数只执行一种任务,你可以为应用创建一个主要的 init() 函数,包含所有应用构造。这种方式能够帮助你简单地修改应用、移除功能,而不需浏览 document 中残存的依赖。

渐进增强

Progressive Enhancement(渐进增强)是一项主要在 Graceful Degredation 上讨论相关细节的开发实践模型。大体上你需要做的就是编写出在任何可用的技术中都能运行的代码。对于 JavaScript,意味着当脚本不可用时(比如在 BlackBerry 上,或者因为一名过分热情的安全警察),你的 web 产品仍需允许用户到达他们的主要目标,不能因为他们无法开启而缺失的 JavaScript 功能就阻止他们访问,或者不想让他们访问。

令人惊讶地是当你面对一个问题时,将会频繁选择建立大量复杂的 JavaScript 脚本去解决它,但其实这个问题可以使用更加简单的解决方式。我遇见过的一个案例是在页面上创建一个允许用户搜索不同数据的搜索框,可以搜索 web、图片、新闻等等。

最初的版本中,不同的数据选项都是链接,会重写表单 action 的 attribute 指向后端不同脚本来表现相应搜索。

问题在于如果 JavaScript 被禁用,这些链接仍然会显示出来,但任何搜索都只会返回标准值,因为表单的 action 没被改变。解决方法很简单:除链接以外我们提供了一个单选按钮组的选项,也使用一个后端脚本分给其相应的搜索脚本。

这样不仅使每个人都能得到正确的搜索结果,也方便统计每个功能选项有多少用户选择。通过使用我们管理的正确 HTML 结构,成功避免利用 JavaScript 同时完成切换表单 action 与点击跟踪脚本的功能,每一位用户都能使用,无需在意环境问题。

允许配置与转换

如何让代码保持可维护性和整洁?最成功的要点之一就是创建一个 configuration object(可配置对象),包含所有可能随时间改变的事物。包括你创建的 element 里面使用到的所有文本(按键值与图片的选择文本)、CSS class 以及 ID 名、你所创建的接口的通用参数。

例如 Easy YouTube player(一个 YouTube 视频下载插件)有着下列配置对象:

如果你将这个作为模块化的一部分,甚至可以使其公有化,以允许操作者在初始化你的模块之前仅需重写他们需要的部分。

保持代码简单地可维护性是十分重要的一件事,避免未来有相关需求的维护者不得不阅读全部代码,寻找他们应该修改的地方。如果这不够显眼,你就是采取了被废弃的或者说非常丑陋的解决方式。一旦需要升级时,不雅的解决方式无法接受补丁,并且完全失去了重用代码的机会。

避免长嵌套

嵌套代码解释了逻辑结构并使其更加易读,但太长的嵌套会让人难以搞清楚你在尝试什么。代码阅读者不应被强迫水平滚动,或在遇见喜欢包裹长串代码的编辑者时遭受困惑(这会使得你尝试缩进的努力毫无意义)。

另一个关于嵌套的问题是变量名和循环。一般你会使用 i 作为开始第一个循环的迭代变量,接下来,你会继续使用 j、k、l 等等。这样很快就会变得凌乱:

就像我正在做的,我使用了常见的 —— 应被抛弃的 —— 变量名 ul 与 li,为了嵌套列表我需要 nestedul 与 datali。如果列表要继续嵌套下去,我就需要更多的变量名,一直持续。更有意义的做法是将为每个成员创建嵌套列表的任务放进各自函数中,并通过恰当的数据调用。这样也能帮助我们防止一个套一个的循环。addMemberData() 函数非常通用,极有可能在其他时间派上用场。经过这些考虑后,我就可以像下面这样重写代码:

优化循环

如果你不能正确使用循环,它们就会变得非常缓慢。最常见的错误之一是在每次迭代判断中读取数组的长度属性:

这意味着循环每次运行,JavaScript 便会读取一次该数组的长度。你可以通过将长度值存储在其他变量中来避免这个问题:

更简短的优化方法是在循环判断块中创建一个第二变量:

另一件需要确定的事情是你已将大计算量的代码放在循环外部,包括正则表达式与 —— 更重要的 —— DOM 处理。你可以在循环中创建 DOM 节点,但不要将它们插入 document。你会在下一节的 DOM 最佳实践中学到更多。

最小化 DOM 访问

在浏览器中访问 DOM 是一件昂贵的事情。DOM 是一个非常复杂的 API,在浏览器中渲染会花费大量时间。运行复杂的 web 应用时,你可以发现你的电脑已被其他工作占满了 —— 修改需要花费更长时间或者只能显示一半等等。

为了确保你的代码足够快速,不会拖累浏览器停止,则应尽量最小化访问 DOM。不要不断地创建和使用 element,而需创建一个工具函数将 string 变为 DOM 元素,然后在生成过程最后调用这个函数影响一次浏览器渲染,而不是不断地干扰。

不要屈服于浏览器的独有特性

以某个浏览器为中心编写代码是一种会让代码难以维护并很快过时的方式。如果你浏览网页,将发现大量脚本特定了某个浏览器,其他浏览器更新版本后就会停止运行。

这是费时费力的行为 —— 就像本篇教程展现的一样,我们应该基于标准创建代码,而不是针对某一个浏览器。web 服务于每个人,不是一群使用最先进配置的精英用户。当浏览器市场快速更新时,你却需要回溯自己的代码并保持修复。这既没有效率也不有趣。

如果仅有一个浏览器拥有一些令人惊讶的工作特性,并且你的确需要使用,将代码放在专属于它的脚本文档中,然后以浏览器和版本号命名。这意味着当该浏览器被废弃时,你可以更轻易地发现和移除这个功能。

不要相信任何数据

谈论代码与数据安全时,要记住的要点之一便是不要相信任何数据。不仅仅因为邪恶的家伙想要窃取你的系统;它起始于简单的可用性。用户总会输入错误的数据。不是因为他们很愚蠢,而是因为太忙了,总被其他事物分心或你指示的词语令他们困惑。比如说我预定了一个月而不是六天的旅馆房间,只因为输入了一个错误的数字。。。我认为自己还是足够聪明的。

简而言之,确保进入系统的所有数据都是清晰且确实需要的。在后台编写从 URL 中检索到的参数时,这是非常重要的。在 JavaScript 中,测试传递给函数的参数类型十分重要(使用关键词 typeof)。当 members 不是一个数组时,下面的代码将会出现错误(例如对于一个 string,它将会为 string 的每个字符创建一个列表):

为了让代码正常工作,你需要检查 members 的类型,确保是一个数组:

数组会设下陷阱,告诉你自己是对象。为了确定它们是数组,可以检验一个只有数组才拥有的方法。

另一个不安全的实践是从 DOM 阅读信息,不做校验就使用。比如说,我曾经不得不调试一些导致 JavaScript 功能中止的代码。这些代码用于 —— 因为我自身的一些原因 —— 在 innerHTML 之外从一个页面元素中读取一个用户名,然后作为参数被一个函数调用。用户名可能是任意包括单引号和引号的 UTF-8 字符。这样将结束任何字符串,剩下的部分就会成为错误数据。并且如果有任意用户使用像 Firebug 或者 Opera DragonFly 这样的工具改变 HTML,就可以将用户名改成任何东西,并注入你的函数。

同样适用于只在客户端验证的表单。我曾经通过重写一个选择以提供另一个选项,注册了一个不可用的 email 地址。因为表单没在后台验证,使得该进程毫无阻碍地运行。

对于 DOM 访问,检验自己尝试访问的元素再修改十分有必要,也是你所期待的 —— 否则代码将会运行失败或造成奇怪的渲染 bug。

用 JavaScript 增加功能,不要创建太多内容

就像你在其他示例中看见的那样,在 JavaScript 中创建大量 HTML 会变得十分缓慢与古怪。特别在 Internet Explorer 上,当它一直通过 innerHTML 加载和操控内容时,如果你修改文档就会遇见各种各样的问题(在 Google 搜索 “操作中止错误” 看看一段悲哀和痛苦的故事)。

在页面维护方面,创建大量 HTML 标记也是一个糟糕的主意,因为不是每一位维护者都拥有和你一样水平的开发技巧,他们很可能对你的代码感到困惑。

我发现当我不得不使用一个大量依赖 JavaScript 的 HTML 模板创建应用时,通过 Ajax 加载这个模板更有用。那样维护者不需要涉及到你的 JavaScript 代码,便可以修改 HTML 结构和重要文本。唯一的障碍就是告诉他们,你需要哪些 ID 以及是否有必要遵循已定义顺序的中心 HTML 结构。你可以用内联 HTML 注释做到这些(然后当你加载好模板后取走这些注释)。示例可以查看 Easy YouTube template 的源代码。

在这个脚本中,当正确的 HTML 容器可用时,加载模板,在后面的 setupPlayer() 方法中应用事件处理程序:

通过此方法,我允许人们以任何需要的方式转换和改变这个插件,而无需修改 JavaScript 代码。

站在巨人的肩上构建

无可否认,近几年 JavaScript 库和框架已经统治了 web 开发市场。这不是坏事 —— 如果它们都能正确使用的话。所有优秀的 JavaScript 库都只做了一件事:简化你的开发生活,不再奔波于浏览器间的不一致,不再不断修复浏览器支持漏洞。JavaScript 库为你提供了一种可测的、基于函数的构建选择。

不通过库初学 JavaScript 是很好的主意,因为你可以切实地知道发生了什么,但当真正开始开发网站时,你需要使用一个 JS 库。你会处理更少的问题,并且出现的 bug 至少都是可以复现的,而不是随机出现的浏览器问题。

我的个人爱好是 Yahoo User Interface Library(YUI),基于 JQueryDojoPrototype,但还有一堆优秀的库,你需要从中找到最适合自己和产品的那个库。

有时所有的库都很适合,在相同的项目中使用几个库可不是一个好主意。这会提升不必要的复杂性和维护难度。

开发环境代码并不等于生产环境代码

最后一点我要谈论的不是 JavaScript 本身,而是如何使它更好地适应你的开发策略。因为JavaScript 的任何修改都会迅速影响你的网站的功能和性能,尽可能优化你的代码是一件很吸引人的事,甚至可以不顾及对于维护性的影响。

这里有许多聪明的技巧,你可以应用到 JavaScript 中让其表现得更棒。另一方面它们中的大部分都伴随着使代码更难以理解和维护的风险。

为了写出健全的、工作稳定的 JavaScript 脚本,我们需要跳出这种循环,停止为机器而不是为其他开发者优化代码。大多数时候,有些在其他语言中是常识的事却不为大部分 JavaScript 开发者所知。一个构建脚本可以移除缩进、注释,用数组查找替代字符串(避免 MSIE 为每个字符串的单独实例创建一个字符串对象 —— 甚至在条件中),并做其他所有需要的细节工作,以让我们的 JavaScript 在浏览器中飞翔。

如果我们更多关注于使原始代码易于理解,方便其他开发者扩展,我们就可以创建出完美的构建脚本。如果我们优化过度,则永远得不到这个结果。不要为你自己或浏览器构建代码 —— 为下一位从你这里接手的开发者构建代码。

总结

JavaScript 的主要诀窍在于避免采用简单的途径。JavaScript 是一种非常通用的语言,并且因为其运行的环境拥有很高的宽容度,十分容易写出看似完成工作的草率代码。然而同样的代码将会在几个月后回来彻底刺伤你。

如果你想拥有一份 web 开发者的工作,JavaScript 开发会成为你知识领域中十分必要的一环。如果你想从现在开始,那么你是幸运的,我自己和其他许多人已经犯了大量错误,完成了所有试验和自我改正;现在我们可以沿着这些知识前行了。

3 13 收藏 1 评论

关于作者:honoka

每次编程都想象自己在写诗 个人主页 · 我的文章 · 20 ·       

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部