资讯专栏INFORMATION COLUMN

15种编写自我文档化的JavaScript方式

tianlai / 928人阅读

摘要:我故意保持示例简单,以说明公共接口是如何自我文档化的。这种类型的函数产生更多的自我文档化代码的另一个原因是你可以信任他们的输出。

在代码里面找到一个完全没有地方或没有用的注释是不是很有趣?

这是一个很容易犯的错误:你改变了一些代码,但忘记删除或更新注释。坏的注释不会破坏你的代码,但你可以想象一下调试时会发生什么。你读了注释,但代码却在做另一件事,也许最终你浪费了一些时间来弄懂它,甚至最坏的情况是,它误导了你。

但没有编写任何注释的代码不是一个选择。在我超过15年的编程经验里,我从来没有见过一个代码库,其中的评论是完全不必要的。

注释不仅有助于使我们的代码更容易理解,也可以帮助我们改进整个程序的设计。

这种类型的编码叫做自我文档化,现在让我来告诉你如何采用这种方式编程。虽然在这里我的例子使用的是JavaScript,但你可以应用到期其他的语言和技术中去。

技术概述

一些程序员把注释作为代码自我文档化的一部分,在本文中,我们只关注代码,注释固然很重要,但它是一个需要多带带讨论的大话题。

我们可以将代码自我文档化的技术分为3大类:

structural(结构):其中代码或目录的结构用于阐明目的

naming related(命名相关):例如函数或变量的命名

syntax related(语法相关):我们利用(或者避免使用)语言的特征来是代码清晰

其中很多是看起来很简单,挑战来自于你要知道什么时候用什么技术。我会告诉你一些实际例子,我们将会处理的每一个例子。

结构

首先,我们来看一下结构类别。结构变化时为了增强代码的清晰度而移动代码。

将代码移动到函数中

这与提取代码重构相同——意味着我们采用现有代码并将其移动到一个新的函数中:我们将代码提取到一个新函数中。

例如,猜想一下下面的代码是做什么的:

var width = (value - 0.5) * 16;

上述代码不是很清楚,此时注释可能是非常有用的,但或许我们可以提取一个函数,使其自我文档化:

var width = emToPixels(value);

function emToPixels(ems) {
    return (ems - 0.5) * 16;
}

唯一的变化时我把计算移动到一个函数里,函数的名称描述了它的作用,所以代码不再需要注释。作为一个额外的好处,我们现在有了一个有用的函数,我们可以在其他地方使用这个函数,这种方法有助于减少代码重复冗余。

用函数替换条件表达式

很多时候带有多个操作数的代码,如果没有注释是很难理解的。我们可以应用类似上述的方法来使代码清晰:

if(!el.offsetWidth || !el.offsetHeight) {
}

上诉条件的目的是什么?

function isVisible(el) {
    return el.offsetWidth && el.offsetHeight;
}

if(!isVisible(el)) {
}

再一次,我们把代码移动到一个函数内,代码立即更容易理解。

用变量替换表达式

用变量替换某个东西类似于将代码移动到一个函数中,而不是一个函数,此时我们只需要一个变量。
让我们再看一下if条件语句的例子:

if(!el.offsetWidth || !el.offsetHeight) {
}

我们还可以通过引入一个变量,而不是提取一个函数,来使我们的代码自我文档化:

var isVisible = el.offsetWidth && el.offsetHeight;
if(!isVisible) {
}

这可能是比提取函数更好的选择,例如,当你当你想要阐明的逻辑对于仅在一个地方使用的某个算法非常特定时。

这种方法最常见的是用于数字表达式:

return a * b + (c / d);

我们可以通过分割计算来使上述代码更清晰:

var multiplier = a * b;
var divisor = c / d;
return multiplier + divisor;

因为我害怕数学,想象上述的例子还是有一些算法的。在任何情况下,代码自我文档化的关键是你可以将复杂的表达式移动到变量中,并增加意义,否则你的代码是难以的理解的。

类和模块接口

类和模块的接口——即公共方法和属性,可以作为其使用的文档。

让我们来看一下这个例子:

class Box {
    setState(state) {
        this.state = state;
    }

    getState() {
        return this.state;
    }
}

这个类可以包含一些其他的代码。我故意保持示例简单,以说明公共接口是如何自我文档化的。

你能告诉我应该如何使用这个类吗?也许有一点点作用,但它不明显。

这两个函数都有合理的名字:它们要做的是阐明自己的名字。但是尽管如此,它不是很清楚你应该如何使用它们,很可能你需要阅读更多的代码或类的文档来弄清楚。

如果我们把它改成这样:

class Box {
    open() {
        this.state = "open";
    }

    close() {
        this.state = "closed";
    }

    isOpen() {
        return this.state === "open";
    }
}

这是更容易理解的用法,你不觉得吗?注意我们只是改变了公共接口,内部表示仍然与this.satte属性相同。

现在你可以一眼就看出Box类是如何使用的了。这表明这表明即使第一个版本的函数具有良好的名称,但完整的包仍然是混乱的,如何通过这样简单的变化,你可以有一个非常大的影响。很多时候你需要想想大局。

代码分组

代码分组的不同部分也可以作为一种文档形式。

例如,你应该将变量声明尽可能地靠近它们被使用的位置,并尝试将变量使用组合在一起。

这可以用于指示代码不同部分之间的关系,以便将来更改它的任何人都可以更容易地找到他们需要查阅的部分。

思考如下的例子:

var foo = 1;

blah()
xyz();

bar(foo);
baz(1337);
quux(foo);

你能一眼看出foo被调用了多少次吗?对比下面的例子:

var foo = 1;
bar(foo);
quux(foo);

blah()
xyz();

baz(1337);

通过把foo的所用用途分组在一起,我们很容易可以看出代码的哪些部分取决于它。

使用纯函数

纯函数比依赖性强的函数更容易理解。

什么是纯函数?当调用一个具有相同参数的函数时,如果它总是产生相同的输出,它很有可能是一个“纯”函数。这意味着纯函数不应该有任何副作用或依赖状态,如时间、对象属性、Ajax等。

这种类型的函数更容易理解,因为影响其输出的任何值都明确传递,你不必弄清楚其中的某个值是什么、来自哪里,或什么因素会影响结果,因为它是一目了然的。

这种类型的函数产生更多的自我文档化代码的另一个原因是你可以信任他们的输出。不管什么时候,函数总是输出基于你传递给它的参数的值,它也不会影响任何的外部代码,所以你可以相信它不会导致意想不到的副作用。

一个很好的例子是,错误地使用document.write(),有经验的JS开发者知道不应该使用它,但是很多初学者都被它绊倒。有时候它工作的很好,但在其他时候,在某些情况下,它可以把整个页面擦干净。谈一个副作用的痛!

为了更好地阐释纯函数是什么,可以查看Functional Programming: Pure Functions。

目录和文件结构

当命名文件或目录时,遵循项目中用到的命名约定。如果项目中没有明确的命名约定,请遵循您选择的语言命名标准。

例如,你要添加有关UI的新的代码,请找到项目中放置类似功能的位置,如果UI相关的代码放在src/ui中,那你应该放置在这里。

基于你已经知道项目中的其他代码段,目录和文件结构清晰使得你更容易找到代码, 并明白其目的。所有的UI代码都放在同一个地方,所以它必须是和UI相关的代码。

命名

这里有一个流行的摘引关于计算机科学的两个艰难的方面:

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

那么,让我们来谈谈如何使用合理的命名来使我们的代码自我文档化。

重命名函数

函数的命名一般不太难,这里有一些简单的规则,你可以遵循:

避免使用handlemanage这样的模糊词:handleLinks(), manageObjects(),这些都做了什么?

使用主动性动词:cutGrass(), sendFile()函数积极地执行了某事

表明返回值:getMagicBullet(), readFile(),这不是你总是可以做到的,但赋予它意义是有帮助的

强类型的语言可以使用类型命名来帮助表明返回值

重命名变量

对于变量,这里有两个好的经验法则:

表明单位:如果有数字参数,可以包含参数的预期单位。例如,widthPX而不是width表明值得单位是像素而不是其他单位

不要使用快捷方式:ab不是可接受的变量名称, 除了在循环计算器中

遵循既定的命名约定

尝试在代码中遵循相同的命名约定。例如,如果你有一个特定类型的对象,调用它相同的名称:

var element = getElement();

不用突然觉得称之为node:

var node = getElement();

如果你遵循与代码库中其他地方相同的命名约定,阅读代码的任何人都可以基于此变量在别的地方的命名含义安全地假设它在此处的含义。

使用有意义的错误

未定义不是一个对象!

每个人的最爱。让我们抛开JavaScript的例子,让我们确保代码抛出的任何错误都是有意义的消息。

什么可以使错误消息有意义?

它应该描述错误是什么

如果可能,它应该包括任何导致错误地变量值或其他数据

关键点:抛出的错误应该帮助我们找出哪里出错了——因此错误消息应该像函数那样告诉我们应该怎么做

语法

自我文档化代码的语法相关方法可以有一些语言特点。例如,Ruby和Perl允许你写一些奇怪的语法技巧,一般来说,应该避免。

让我们来看几个在JavaScript中遇到的问题:

不要使用语法技巧

不要使用语法技巧。这很容易让人疑惑:

imTricky && doMagic();

上面的这行代码相当于如下更健全的代码:

if(imTricky) {
    doMagic();
}

习惯使用后一种写法,语法技巧并不讨任何人的喜欢。

使用常量命名,避免使用magic值

如果你的代码中有特殊值——例如数字或字符串值,请考虑使用常量命名。即使现在看起来很清楚,但在一个月或者两个月后,没人会知道为什么这么一个特定的号码放在那里,意义是什么。

const MEANING_OF_LIFE = 42;

(如果你不使用ES6,你可以用var,是一样的。)

避免使用布尔值

布尔值会让人难以理解代码,考虑这个:

myThing.setData({ x: 1 }, true);

此处true的作用是什么呢?除非找到setDate()方法并阅读它。

相反你可以添加另一个函数,或重命名现有的函数:

myThing.mergeData({ x: 1 });

现在,你立即就可以知道这行代码发生了什么。

使用语言优势

我们甚至可以使用我们编写的语言的一些特征来更好地表述代码背后的意义。

JavaScript中一个很好的例子是数组的迭代:

var ids = [];
for(var i = 0; i < things.length; i++) {
  ids.push(things[i].id);
}

上面的代码将一个ID列表收集到一个新的数组中,但是为了理解这块代码是做什么的,我们需要阅读整个循环的全部。下面我们使用map()来进行比较:

var ids = things.map(function(thing) {
  return thing.id;
});

在这种情况下,我们立即知道这会产生一系列的新东西因为这是map()的目的。如果你有更复杂的循环逻辑,这是很有益的写法。list of other iteration functions on MDN

JavaScript的另一个好例子是const关键字。

通常,你声明的变量值应该永远不会改变,一个常见的例子是使用CommonJS加载模块时:

var async = require("async");

你可以用如下写法做出不糊改变意图的语句:

const async = require("async");

作为一个额外的好处,如果有人不小心试图改变这一点,我们将会得到一个错误。

反模式

通过所有这些方法,你可以做很多事情,但是,有些事情你应该注意。

Extracting for the sake of having short functions

有些人主张使用简短的小函数,如果你把所有东西都提取出来,那就是你能得到的。但是,这可能不利于代码的理解程度。

例如,假设你正在调试一些代码。你想查看a()函数,然后你会发现b()函数,接着你会发现使用到c()函数,等等。

虽然简短的功能可以很好而且易于理解,但如果你只在一个地方使用该功能,那么请考虑使用replace expression with variable方法。

别强迫

像往常那样,没有绝对正确地方法来使代码自我文档化。因此,如果某些东西似乎是一个好主意,但不能强制使用。

总结

使你的代码自我文档化可以大大提高代码的可维护性,每个注释都是需要额外维护的,所以在有可能删除注释的情况下,编写自我文档化的代码是一个好选择。

但是自我文档化的代码并不能取代文档或者注释,例如,代码本身在表达意图的时候收到限制时,你还是需要有很好的注释的。在一些库中,API文档是很重要的,因此单纯靠阅读代码是不可取的,除非你的库非常小。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82146.html

相关文章

  • 前端开发之基础知识-HTML(一)

    摘要:文档规范文档规范制定了文档的编写规范,可部分遵守,也可全部遵守,看开发要求。标签行内元素,表示一行中的一小段内容,没有具体的语义。表示当前文件所在目录下的上一级目录,比如表示当前目录下的上一级目录下的文件夹中的的图片。 1.1 html概述和基本结构 html概述 HTML是 HyperText Mark-up Language 的首字母简写,意思是超文本标记语言,超文本指的是超链接,标记指...

    番茄西红柿 评论0 收藏0
  • larkplayer: 插件化的 HTML5 播放器

    摘要:是一款轻量级易扩展的播放器,是为解决一些中小型的视频业务场景。同时各插件由于是面向的播放器接口,插件不知道插件的存在,因此能极大地降低各插件功能间的耦合。 larkplayer 是一款轻量级 & 易扩展的 html5 播放器,是为解决一些中小型的视频业务场景。这些业务不一定需要大而全的解决方案,并且他们往往有自己的定制化需求。 背景 为什么要编写 larkplayer?(注意,这里面有...

    lijy91 评论0 收藏0
  • larkplayer: 插件化的 HTML5 播放器

    摘要:是一款轻量级易扩展的播放器,是为解决一些中小型的视频业务场景。同时各插件由于是面向的播放器接口,插件不知道插件的存在,因此能极大地降低各插件功能间的耦合。 larkplayer 是一款轻量级 & 易扩展的 html5 播放器,是为解决一些中小型的视频业务场景。这些业务不一定需要大而全的解决方案,并且他们往往有自己的定制化需求。 背景 为什么要编写 larkplayer?(注意,这里面有...

    linkin 评论0 收藏0
  • 一个前端程序猿的Sublime Text3的自我修养

    摘要:效果如下配置方法参考下的配置方法完美支持提供了比默认更好的语法高亮,而且他完美支持。语法高亮默认安装的对的支持让人抓狂,帧动画别开玩笑了你只会看到一片白色的纯文本一样的代码。事实上不光,我建议用完全替代原来的来完成语法高亮。 文章转载自本人的博客《三省吾身丶丶》点击查看喜欢的话请疯狂的推荐吧! ^_^ 本文章会在本人有插件或者设置更新时,进行不定时更新 偷懒了,图片地址直接设置的博客...

    KunMinX 评论0 收藏0

发表评论

0条评论

tianlai

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<