JavaScript代码风格要素

原文地址javascript

本文已在前端早读课公众号首发:【第952期】JavaScript代码风格要素前端

译者:墨白 校对:野草java


1920年,由威廉·斯特伦克(William Strunk jr .)撰写的《英语写做手册:风格的要素(The Elements of Style)》出版了,这本书列举了7条英文写做的准则,过了一个世纪,这些准则并无过期。对于工程师来讲,你能够在本身的编码风格中应用相似的建议来指导平常的编码,提升本身的编码水平。es6

须要注意的是,这些准则不是一成不变的法则。若是违背它们,可以让代码可读性更高,那么便没有问题,但请特别当心并时刻反思。这些准绳是经受住了时间考验的,有充分的理由说明:它们一般是正确的。若是要违背这些规则,必定要有充足的理由,而不要单凭一时的兴趣或者我的的风格偏好。web

书中的写做准则以下:编程

  • 以段落为基本单位:一段文字,一个主题。数组

  • 删减无用的语句。promise

  • 使用主动语态。浏览器

  • 避免一连串松散的句子。服务器

  • 相关的内容写在一块儿。

  • 从正面利用确定语句去发表陈述。

  • 不一样的概念采用不一样的结构去阐述。

咱们能够应用类似的理念到代码编写上面:

  1. 一个function只作一件事,让function成为代码组合的最小单元。

  2. 删除没必要要的代码。

  3. 使用主动语态。

  4. 避免一连串结构松散的,不知所云的代码。

  5. 将相关的代码写在一块儿。

  6. 利用判断true值的方式来编写代码。

  7. 不一样的技术方案利用不一样的代码组织结构来实现。

1.一个function只作一件事,让function成为代码组合的最小单元

软件开发的本质是“组合”。 咱们经过组合模块,函数和数据结构来构建软件。理解若是编写以及组合方法是软件开发人员的基本技能。

模块是一个或多个function和数据结构的简单集合,咱们用数据结构来表示程序状态,只有在函数执行以后,程序状态才会发生一些有趣的变化。

JavaScript中,能够将函数分为3种:

  • I/O 型函数 (Communicating Functions):函数用来执行I/O。

  • 过程型函数 (Procedural Functions):对一系列的指令序列进行分组。

  • 映射型函数 (Mapping Functions):给定一些输入,返回对应的输出。

有效的应用程序都须要I/O,而且不少程序都遵循必定的程序执行顺序,这种状况下,程序中的大部分函数都会是映射型函数:给定一些输入,返回相应的输出。

每一个函数只作一件事情:若是你的函数主要用于I/O,就不要在其中混入映射型代码,反之亦然。严格根据定义来讲,过程型函数违反了这一指导准则,同时也违反了另外一个指导准则:避免一连串结构松散,不知所云的代码。

理想中的函数是一个简单的、明确的纯函数:

  • 一样的输入,老是返回一样的输出。

  • 无反作用。

也能够查看,“什么是纯函数?”

2. 删除没必要要的代码

简洁的代码对于软件而言相当重要。更多的代码意味更多的bug隐藏空间。更少的代码 = 更少的bug隐藏空间 = 更少的bug

简洁的代码读起来更清晰,由于它拥有更高的“信噪比”:阅读代码时更容易从较少的语法噪音中筛选出真正有意义的部分。能够说,更少的代码 = 更少的语法噪声 = 更强的代码含义信息传达

借用《风格的元素》这本书里面的一句话就是:简洁的代码更健壮。

function secret (message) {
  return function () {
    return message;
  }
};

能够简化成:

const secret = msg => () => msg;

对于那些熟悉简洁箭头函数写法的开发来讲,可读性更好。它省略了没必要要的语法:大括号,function关键字以及return语句。

而简化前的代码包含的语法要素对于传达代码意义自己做用并不大。它存在的惟一意义只是让那些不熟悉ES6语法的开发者更好的理解代码。

ES6自2015年已经成为语言标准,是时候去学习它了。

删除没必要要的代码

有时候,咱们试图为没必要要的事物命名。问题是人类的大脑在工做中可用的记忆资源有限,每一个名称都必须做为一个单独的变量存储,占据工做记忆的存储空间。

因为这个缘由,有经验的开发者会尽量地删除没必要要的变量。

例如,大多数状况下,你应该省略仅仅用来当作返回值的变量。你的函数名应该已经说明了关于函数返回值的信息。看看下面的:

const getFullName = ({firstName, lastName}) => {
  const fullName = firstName + ' ' + lastName;
  return fullName;
};

对比

const getFullName = ({firstName, lastName}) => (
  firstName + ' ' + lastName
);

另外一个开发者一般用来减小变量名的作法是,利用函数组合以及point-free-style

Point-free-style是一种定义函数方式,定义成一种与参数无关的合成运算。实现point-free风格经常使用的方式包括函数科里化以及函数组合。

让咱们来看一个函数科里化的例子:

const add2 = a => b => a + b;
// Now we can define a point-free inc()
// that adds 1 to any number.
const inc = add2(1);
inc(3); // 4

看一下inc()函数的定义方式。注意,它并未使用function关键字,或者=>语句。add2也没有列出一系列的参数,由于该函数不在其内部处理一系列的参数,相反,它返回了一个知道如何处理参数的新函数。

函数组合是将一个函数的输出做为另外一函数的输入的过程。 也许你没有意识到,你一直在使用函数组合。链式调用的代码基本都是这个模式,好比数组操做时使用的.map(),Promise 操做时的promise.then()。函数组合在函数式语言中也被称之为高阶函数,其基本形式为:f(g(x))。

当两个函数组合时,无须建立一个变量来保存两个函数运行时的中间值。咱们来看看函数组合是怎么减小代码的:

const g = n => n + 1;
const f = n => n * 2;
// 须要操做参数、而且存储中间结果
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};
incThenDoublePoints(20); // 42

// compose2 - 接受两个函数做为参数,直接返回组合
const compose2 = (f, g) => x => f(g(x));
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

你能够利用函子(functor)来作一样的事情。在函子中把参数封装成可遍历的数组。让咱们利用函子来写另外一个版本的compose2

const compose2 = (f, g) => x => [x].map(g).map(f).pop();
const incThenDoublePointFree = compose2(f, g);
incThenDoublePointFree(20); // 42

当每次使用promise链时,你就是在作这样的事情。

几乎每个函数式编程类库都提供至少两种函数组合方法:从右到左依次运行的compose();从左到右依次运行的pipe()

Lodash中的compose()以及flow()分别对应这两个方法。下面是使用pipe的例子:

import pipe from 'lodash/fp/flow';
pipe(g, f)(20); // 42

下面的代码也作着一样的事情,但代码量并未增长太多:

const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
pipe(g, f)(20); // 42

若是函数组合这个名词听起来很陌生,你不知道如何使用它,请仔细想想:

软件开发的本质是组合,咱们经过组合较小的模块,方法以及数据结构来构建应用程序。

不难推论,工程师理解函数和对象组合这一编程技巧就如同搞装修须要理解钻孔机以及气枪同样重要。

当你利用“命令式”代码将功能以及中间变量拼凑在一块儿时,就像疯狂使用胶带和胶水将这些部分胡乱粘贴起来同样,而函数组合看上去更流畅。

记住:

  • 用更少的代码。

  • 用更少的变量。

3. 使用主动语态

主动语态比被动语态更直接,跟有力量,尽可能多直接命名事物:

  • myFunction.wasCalled()优于myFunction.hasBeenCalled()

  • createUser优于User.create()

  • notify()优于Notifier.doNotification()

命名布尔返回值时最好直接反应其输出的类型:

  • isActive(user)优于getActiveStatus(user)

  • isFirstRun = false;优于firstRun = false;

函数名采用动词形式:

  • increment()优于plusOne()

  • unzip()优于filesFromZip()

  • filter(fn, array)优于matchingItemsFromArray(fn, array)

事件处理

事件处理以及生命周期函数因为是限定符,比较特殊,就不适用动词形式这一规则;相比于“作什么”,它们主要用来表达“何时作”。对于它们,能够“<何时去作>,<动做>”这样命名,朗朗上口。

  • element.onClick(handleClick)优于element.click(handleClick)

  • element.onDragStart(handleDragStart)优于component.startDrag(handleDragStart)

上面两例的后半部分,它们读起来更像是正在尝试去触发一个事件,而不是对其做出回应。

生命周期函数

对于组件生命周期函数(组件更新以前调用的方法),考虑一下如下的命名:

  • componentWillBeUpdated(doSomething)

  • componentWillUpdate(doSomething)

  • beforeUpdate(doSomething)

第一个种咱们使用了被动语态(将要被更新而不是将要更新)。这种方式很口语化,但含义表达并无比其它两种方式更清晰。

第二种就好多了,但生命周期函数的重点在于触发处理事件。componentWillUpdate(handler)读起来就好像它将当即触发一个处理事件,但这不是咱们想要表达的。咱们想说,“在组件更新以前,触发事件”。beforeComponentUpdate()能更清楚的表达这一想法。

进一步简化,由于这些方法都是组件内置的。在方法名中加入component是多余的。想想若是你直接调用这些方法时:component.componentWillUpdate()。这就好像在说,“吉米吉米在晚餐吃牛排。”你没有必要听到同一个对象的名字两次。显然,

  • component.beforeUpdate(doSomething)优于component.beforeComponentUpdate(doSomething)

函数混合是指将方法做为属性添加到一个对象上面,它们就像装配流水线给传进来的对象加上某些方法或者属性。

我喜欢用形容词来命名函数混合。你也能够常用"ing"或者"able"后缀来找到有意义的形容词。例如:

  • const duck = composeMixins(flying, quacking);

  • const box = composeMixins(iterable, mappable);

4.避免一连串结构松散的,不知所云的代码

开发人员常常将一系列事件串联在一个进程中:一组松散的、相关度不高的代码被设计依次运行。从而很容易造成“意大利面条”代码。

这种写法常常被重复调用,即便不是严格意义上的重复,也只有细微的差异。例如,界面不一样组件之间几乎共享相同的核心需求。 其关注点能够分解成不一样生命周期阶段,并由单独的函数方法进行管理。

考虑如下的代码:

const drawUserProfile = ({ userId }) => {
  const userData = loadUserData(userId);
  const dataToDisplay = calculateDisplayData(userData);
  renderProfileData(dataToDisplay);
};

这个方法作了三件事:获取数据,根据获取的数据计算view的状态,以及渲染。

在大部分现代前端应用中,这些关注点中的每个都应该考虑分拆开。经过分拆这些关注点,咱们能够轻松地为每一个问题提供不一样的函数。

好比,咱们能够彻底替换渲染器,它不会影响程序的其余部分。例如,React的丰富的自定义渲染器:适用于原生iOS和Android应用程序的ReactNative,WebVR的AFrame,用于服务器端渲染的ReactDOM/Server 等等...

drawUserProfile的另外一个问题就是你不能在没有数据的状况下,简单地计算要展现的数据并生成标签。若是数据已经在其余地方加载过了会怎么样,就会作不少重复和浪费的事情。

分拆关注点也使得它们更容易进行测试。我喜欢对个人应用程序进行单元测试,并在每次修改代码时查看测试结果。可是,若是咱们将渲染代码和数据加载代码写在一块儿,我不能简单地将一些假数据传递给渲染代码进行测试。我必须从端到端测试整个组件。而这个过程当中,因为浏览器加载,异步I/O请求等等会耗费时间。

上面的drawUserProfile代码不能从单元测试测试中获得即时反馈。而分拆功能点容许你进行单独的单元测试,获得测试结果。

上文已经已经分析出单独的功能点,咱们能够在应用程序中提供不一样的生命周期钩子给其调用。 当应用程序开始装载组件时,能够触发数据加载。能够根据响应视图状态更新来触发计算和渲染。

这么作的结果是软件的职责进一步明确:每一个组件能够复用相同的结构和生命周期钩子,而且软件性能更好。在后续开发中,咱们不须要重复相同的事。

5.功能相连的代码写在一块儿

许多框架以及boilerplates规定了程序文件组织的方法,其中文件按照代码类别分组。若是你正在构建一个小的计算器,获取一个待办事宜的app,这样作是很好的。可是对于较大的项目,经过业务功能特性将文件分组在一块儿是更好的方法。

按代码类别分组:

.
├── components
│   ├── todos
│   └── user
├── reducers
│   ├── todos
│   └── user
└── tests
    ├── todos
    └── user

按业务功能特性分组:

.
├── todos
│   ├── component
│   ├── reducer
│   └── test
└── user
    ├── component
    ├── reducer
    └── test

当你经过功能特性来将文件分组,你能够避免在文件列表上下滚动,查找编辑所须要的文件这种状况。

6.利用判断true值的方式来编写代码

要作出肯定的断言,避免使用温顺、无色、犹豫的语句,必要时使用 not 来否认、拒绝。典型的

  • isFlying优于isNotFlying

  • late优于notOneTime

if语句

if (err) return reject(err);

// do something

优于

if (!err) {
  // ... do something
} else {
  return reject(err);
}

三元判断语句

{
  [Symbol.iterator]: iterator ? iterator : defaultIterator
}

优于

{
  [Symbol.iterator]: (!iterator) ? defaultIterator : iterator 
}

恰当的使用否认

有时候咱们只关心一个变量是否缺失,若是经过判断true值的方式来命名,咱们得用!操做符来否认它。这种状况下使用 "not" 前缀和取反操做符不如使用否认语句直接。

  • if (missingValue)优于if (!hasValue)

  • if (anonymous)优于if (!user)

  • if (!isEmpty(thing))优于if (notDefined(thing))

函数调用时,避免用null以及undefined代替某一个参数

不要在函数调用时,传入undefined或者null做为某个参数的值。若是某些参数能够缺失,更推荐传入一个对象:

const createEvent = ({
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
}) => ({ title, description, timeStamp });

const birthdayParty = createEvent({
  title: 'Birthday Party',
  description: 'Best party ever!'
});

优于

const createEvent = (
  title = 'Untitled',
  timeStamp = Date.now(),
  description = ''
) => ({ title, description, timeStamp });

const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // This was avoidable
  'Best party ever!'  
);

不一样的技术方案利用不一样的代码组织结构来实现

迄今为止,应用程序中未解决的问题不多。最终,咱们都会一次又一次地作着一样的事情。当这样的场景发生时,意味着代码重构的机会来啦。分辨出相似的部分,而后抽取出可以支持每一个不一样部分的公共方法。这正是类库以及框架为咱们作的事情。

UI组件就是一个很好的例子。10 年前,使用 jQuery 写出把界面更新、应用逻辑和数据加载混在一块儿的代码是再常见不过的。渐渐地,人们开始意识到咱们能够将MVC应用到客户端的网页上面,随后,人们开始将model与UI更新逻辑分拆。

最终,web应用普遍采用组件化这一方案,这使得咱们可使用JSX或HTML模板来声明式的对组件进行建模。

最终,咱们就能用彻底相同的方式去表达全部组件的更新逻辑、生命周期,而不用再写一堆命令式的代码

对于熟悉组件的人,很容易看懂每一个组件的原理:利用标签来表示UI元素,事件处理器用来触发行为,以及用于添加回调的生命周期钩子函数,这些钩子函数将在必要时运行。

当咱们对于相似的问题采用相似的模式解决时,熟悉这个解决模式的人很快就能理解代码是用来作什么的。

结论:代码应该简单而不是过于简单化

尽管在2015,ES6已经标准化,但在2017,不少开发者仍然拒绝使用ES6特性,例如箭头函数,隐式return,rest以及spread操做符等等。利用本身熟悉的方式编写代码实际上是一个幌子,这个说法是错误的。只有不断尝试,才可以渐渐熟悉,熟悉以后,你会发现简洁的ES6特性明显优于ES5:与语法结构偏重的ES5相比,简洁的es6的代码很简单。

代码应该简单,而不是过于简单化。

简洁的代码有如下优点:

  • 更少的bug可能性

  • 更容易去debug

但也有以下弊端:

  • 修复bug的成本更高

  • 有可能引用更多的bug

  • 打断了正常开发的流程

简洁的代码一样:

  • 更易写

  • 更易读

  • 更好去维护

清楚本身的目标,不要毫无头绪。毫无头绪只会浪费时间以及精力。投入精力去训练,让本身熟悉,去学习更好的编程方式,以及更有更有活力的代码风格。

代码应该简单,而不是简单化。

相关文章
相关标签/搜索