图片 15

JavaScript 编码指南

出其不意

1920年,William Strunk
Jr的《英文写作指南》出版了,这本书给英语的风格定下了一个规范,而且已经沿用至今。代码其实也可以使用相似的方法加以改进。

本文接下来的部分是一些指导方针,不是一成不变的法律。如果能够清晰解释代码含义,当然有很多的理由不这样做,但是,请保持警惕和自觉。他们能经过时间的检验也是有理由的:因为他们通常都是对的。偏离指南应该有好的理由,并不能简单因为突发奇想或者个人偏好就那么做。

图片 1

基本上写作的基本准则的每一部分都能应用在代码上:

  • 让段落成为文章的基本结构:每一段对应一个主题。
  • 去掉无用的单词。 .
  • 使用主动语态。
  • 避免一连串松散的句子。
  • 将相关的词语放在一起。
  • 陈述句用主动语态。
  • 平行的概念用平行的结构。

这些都可以用在我们的代码风格上。

  1. 让函数成为代码的基本单元。每个函数做一件事。
  2. 去掉无用的代码
  3. 使用主动语态
  4. 避免一连串松散结构的代码
  5. 把相关的代码放在一起。
  6. 表达式和陈述语句中使用主动语态。
  7. 用并行的代码表达并行的概念。

本文接下来的部分是一些JavaScript
指导方针,不是一成不变的法律。如果能够清晰解释代码含义,当然有很多的理由不这样做,但是,请保持警惕和自觉。他们能经过时间的检验也是有理由的:因为他们通常都是对的。偏离指南应该有好的理由,并不能简单因为突发奇想或者个人偏好就那么做。

原文地址

1、让函数成为代码的基本单元。每个函数做一件事。

软件开发的本质就是写作。我们把模块、函数、数据结构组合在一起,就有了一个软件程序。

理解如何编写函数并如何构建它们,是软件开发者的基本技能。

模块是一个或多个函数或数据结构的简单集合,数据结构是我们如何表示程序的状态,但在没有应用函数,数据结构自身不会发生什么有趣的事情。

JavaScript有三种类型的函数:

  • 交流型函数:执行I/O的函数
  • 功能型函数:一系列指令的合集
  • 映射型函数:给一些输入,返回相应的输出

所有有用的程序都需要I /
O,并且许多程序遵循一些程序顺序,但大多数函数应该像映射函数:给定一些输入,该函数将返回一些相应的输出。

一个函数做一件事:如果你的函数是I/O敏感,那么就不要把I/O和映射(计算)混杂在一起。如果你的函数是为了映射,那么就不要加入I/O。功能性的函数就违背了这条准则。功能性的函数还违背了另一条准则:避免把松散的句子写在一起。

理想的函数应该是一个简单的,确定的,纯粹功能函数。

  • 给定相同的输入,返回相同的输出
  • 没有副作用

参见《什么是纯粹的函数》

图片 2

译者:墨白 校对:野草

2. 去掉无用代码

刚健的文字是简练的。一句话应该不包含无用的词语,一段话没有无用的句子,正如作画不应该有多余的线条,一个机器没有多余的零件。这就要求作者尽量用短句子,避免罗列所有细节,在大纲里就列出主题,而不是什么都说。William
Strunk,Jr.,《英文写作指南》

简练的代码在软件中也很重要,这是因为更多的代码让bug有了藏匿的空间。更少的代码=更少的含有bug的空间=更少bug。

简练的代码更清晰,是因为它有更高的信噪比:读者可以减少对的语法理解更多的了解它的意义。更少的代码=更少的语法噪音=更多信息的传递。

借用《英文写作指南》的一个词:简练的代码更有力

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

上面一段代码可以简化为:

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

对于熟悉箭头函数(ES
2015年加入的新特性)的人来说,这段代码可读性增强了。它去掉了多余的语法:括号,function关键词,以及return返回值语句。

第一个版本包含了不必要的语法。对于熟悉箭头语法的人来说,括号,function关键词,和return语句都没有任何意义。它们存在只是因为还有很多人对ES6的新特性不熟悉。

ES6从2015年就是语言标准了。你应该熟悉它了。

基本上写作的基本准则的每一部分都能应用在代码上:

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

图片 3

Out of the Blue — Iñaki Bolumburu (CC BY-NC-ND 2.0)

1920年,由威廉·斯特伦克(William Strunk jr
.)撰写的《英语写作手册:风格的要素(The Elements of
Style)》出版了,这本书列举了7条英文写作的准则,过了一个世纪,这些准则并没有过时。对于工程师来说,你可以在自己的编码风格中应用类似的建议来指导日常的编码,提高自己的编码水平。

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

书中的写作准则如下:

  • 以段落为基本单位:一段文字,一个主题。
  • 删减无用的语句。
  • 使用主动语态。
  • 避免一连串松散的句子。
  • 相关的内容写在一起。
  • 从正面利用肯定语句去发表陈述。
  • 不同的概念采用不同的结构去阐述。

我们可以应用相似的理念到代码编写上面:

  1. 一个function只做一件事,让function成为代码组合的最小单元。
  2. 删除不必要的代码。
  3. 使用主动语态。
  4. 避免一连串结构松散的,不知所云的代码。
  5. 将相关的代码写在一起。
  6. 利用判断true值的方式来编写代码。
  7. 不同的技术方案利用不同的代码组织结构来实现。

去掉无用的变量

有时候我们倾向给一些实际不需要命名的变量命名。原因是人脑在可用的容量内只能存储有限的资源,并且每个变量都必须作为离散量子存储,占据了我们可用的不多的记忆空间。

因为这个原因,有经验的开发者都倾向减少不需要的变量命名。

比如,在大多数情况下,你应该去掉变量,只给创建一个返回值的变量。函数名应该能够提供足够多的信息以显示它的返回值。看下面的例子:

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

以及:

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

开发者常常用来减少变量的另一个做法是:利用函数组合以及Point-free
的风格。

Point-free
风格是指:定义函数时无需引用对其操作的参数。常用的point-free风格方式主要是curry和函数组合。

看一个使用curry的例子:

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关键词,或者=>语法。没有参数列表,因为这个函数内部并没有使用参数列表。相反的,它返回的是如何处理参数的一个函数。

下面我们看一下使用函数组合的例子。函数组合是把一个函数结果应用到另一个函数的处理流程。你可能没有意识到,你其实一直都在用函数组合。当你调用.map()或者promise.then()函数的时候,你就在使用它了。例如,它的大部分时候的基本形态,其实都像这样:f(g(x)).在代数中,这样的组合被写成:f
∘ g, 被称作“g后f”或者“f组合g”。

当你把两个函数组合在一起时,你就去掉了需要存储的中间返回值的变量。我们看一下下面这个可以更简单的代码:

const g = n => n + 1;
const f = n => n * 2;

// With points:
const incThenDoublePoints = n => {
  const incremented = g(n);
  return f(incremented);
};

incThenDoublePoints(20); // 42

// compose2 - Take two functions and return their composition
const compose2 = (f, g) => x => f(g(x));

// Point-free:
const incThenDoublePointFree = compose2(f, g);

incThenDoublePointFree(20); // 42

使用仿函数也能实现类似的效果。使用仿函数也能实现类似的效果。下面这段代码就是使用仿函数的一个例子:

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()。当我在Lodash里使用它们时,一般都这样引入:

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

如果函数组合对你来说像外星人一样深不可测,而且你也不确定如何使用,那么请认真回顾一下前面的话:

软件开发的本质是写作。我们把模块、函数、数据结构组合在一起,就构成了软件程序。

由此你就可以得出结论:理解函数的工具意义和对象组合,就像是一个家庭手工劳动者要能理解如何使用钻子和钉子枪一样的基本技能。

当你用指令集和中间变量把不同函数组合在一起时,其实就像是用胶布和疯狂的胶水随意的把东西沾在一起。

请记住:

  • 如果能用更少的代码表达相同的意思,且不改变或混淆代码含义,那就应该这样做。
  • 如果可以使用更少变量达到相同目的,也不会改变或混淆原意,那也应该这样做。

让段落成为文章的基本结构:每一段对应一个主题。

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

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

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

JavaScript中,可以将函数分为3种:

  • I/O 型函数 (Communicating Functions):函数用来执行I/O。
  • 过程型函数 (Procedural Functions):对一系列的指令序列进行分组。
  • 映射型函数 (Mapping Functions):给定一些输入,返回对应的输出。

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

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

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

  • 同样的输入,总是返回同样的输出。
  • 无副作用。

也可以查看,“什么是纯函数?”

3.使用主动语态

主动语态比被动语态更加直接、有力。 — William Strunk,Jr.
《英文写作指南》

命名越直接越好。

  • 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)
  • component.onDragStart(handleDragStart)优于component.startDrag(handleDragStart)。

这个例子里两种命名方法的第二种,看上去更像是我们尝试触发一件事,而不是对这个事件作出响应。

去掉无用的单词。 .

2. 删除不必要的代码

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

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

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

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

可以简化成:

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

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

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

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

生命周期函数

假设有一个组件,有这样一个生命周期函数,在它更新之前要调用一个事件处理的函数,有以下几种命名方式:

  • componentWillBeUpdated(doSomething)
  • componentWillUpdate(doSomething)
  • componentWillUpdate(doSomething)

第一种命名使用被动语态。这种方式有点绕口,不如其他方式直观。

第二种方式稍好,但是给人的意思是这个生命周期方法要调用一个函数。componentWillUpdate(handler)读起来就像是这个组件要更新一个事件处理程序,这就偏离了本意。我们的原意是:”在组件更新前,调用事件处理”beforeComponentUpdate()这样命名更为恰当清晰。

还能更精简。既然这些都是方法,那么主语(也就是组件本身)其实已经确定了。调用这个方法时再带有主语就重复了。想象一下看到这段代码时,你会看到component.componentWillUpdate()。这就像是在说“吉米,吉米中午要吃牛排”。你其实不需要听到重复的名字。

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

Functional
mixins 是把属性和方法添加到Object对象上的一种方法。函数一个接一个的组合添加在一起,就像是管道流一样,或者像组装线一样。每个functional
mixin的函数都有一个instance作为输入,把一些额外的东西附加上去,然后再传递给下一个函数,就像组装流水线一样。

我倾向用形容词命名mixin
函数。你也可以使用“ing”或者“able”之类的后缀来表示形容词的含义。例如:

  • const duck = composeMixins(flying,quacking);
  • const box = composeMixins(iterable,mappable);

使用主动语态。

删除不必要的代码

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

由于这个原因,有经验的开发者会尽可能地删除不必要的变量。

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

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

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

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

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

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

记住:

  • 用更少的代码。
  • 用更少的变量。

4、避免一连串松散的语句

一连串的句子很快就会无聊冗长了。William Strunk,Jr.,《英文写作指南》

开发者其实常常讲一连串的事件连接成一整个处理过程:一系列松散的语句本来就为了一个接一个而设计存在的。但过度使用这样的流程会导致代码像意大利面一样错综复杂。

这种序列常常被重复,尽管会有些许的不同,有时还会出乎意料的偏离正规。例如,一个用户界面可能会和另外的用户界面共享了同样的组件代码。这样的代价就是代码可能被分到不同的生命周期里并且一个组件可能由多个不同的代码块进行管理。

参考下面这个例子:

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

这段代码做了三件事:加载数据,计算相关状态,然后渲染内容。

在现代的前端应用框架中,这三件事是相互分离的。通过分离,每件事都可以得到比较好的组合或者扩展。

例如,我们可以完全替换渲染器,而不用影响其他部分;例如,React有丰富的自定义渲染器:适用于原生iOS和Android应用程序的ReactNative,WebVR的AFrame,用于服务器端渲染的ReactDOM
/ Server 等等。

另一个问题是你无法简单的计算要显示的数据并且如果没有第一次加载数据就无法生成显示页面。假如你已经加载了数据呢?那么你的计算逻辑就在接下来的调用中变的多余了。

分离也使得各个部件独立可测。我喜欢给自己的应用加很多单元测试,并且把测试结果显示出来,这样我有任何改动的时候都能看到。但是,如果我要尝试测试加载数据并渲染的功能,那我就不能只用一些假数据测试渲染部分。正在保存……

我无法通过单元测试立刻获得结果。函数分离却可以让我们能够进行独立的测试。

这个例子就已经说明,分离函数可以让我们能够参与到应用的不同生命周期中去。可以在应用加载组件后,触发数据的加载功能。计算和渲染可以在视图发生变化的时候进行。

这样的结果就是更清楚地描述了软件的责任:可以重用组件相同的结构以及生命周期的回调函数,性能也更好;在后面工作流程中,我们也节省了不必要的劳动。

避免一连串松散的句子。

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)

5.把相关的代码放在一起。

很多框架或者样板程序都预设了一种程序的组织方法,那就是按照文件类型划分。如果你做一个小的计算器或者To
Do的应用,这样做没问题;但是如果是大型项目,更好的办法是按功能对文件进行分组。

下面以一个To Do 应用为例,有两种文件组织结构。

按照文件类型分类

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

按照文件功能分类

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

按照功能组织文件,可以有效避免在文件夹视图中不断的上下滚动,直接去到功能文件夹就可以找到要编辑的文件了。

把文件按照功能进行组织。

将相关的词语放在一起。

事件处理

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

  • element.onClick(handleClick)优于element.click(handleClick)
  • element.onDragStart(handleDragStart)优于component.startDrag(handleDragStart)

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

6.陈述句和表达式使用主动语态。

“做出明确的断言。避免无聊、不出彩、犹豫、不可置否的语气。使用“not”时应该表达否定或者对立面的意思,而不要用来作为逃避的手段。”William
Strunk,Jr., 《英文写作指南》。

  • isFlying优于isNotFlying
  • late优于notOnTime

陈述句用主动语态。

生命周期函数

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

  • 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);

If语句

if (err) return reject(err);

// do something...

比下面这种方式更好:

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

平行的概念用平行的结构。

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代码不能从单元测试测试中得到即时反馈。而分拆功能点允许你进行单独的单元测试,得到测试结果。

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

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

三元表达式

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

比下面的形式更好:

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

这些都可以用在我们的代码风格上。

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

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

按代码类别分组:

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

按业务功能特性分组:

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

当你通过功能特性来将文件分组,你可以避免在文件列表上下滚动,查找编辑所需要的文件这种情况。

尽量选择语气强烈的否定句

有时候我们只关系一个变量是否缺失,因此使用主动语法会让我们被迫加上一个!。在这些情况下,不如使用语气强烈的否定句式。“not”这个词和!的语气相对较弱。

  • if (missingValue)优于if (!hasValue)
  • if (anonymous)优于if (!user)
  • if (isEmpty(thing))优于if (notDefined(thing))

让函数成为代码的基本单元。每个函数做一件事。

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

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

  • isFlying优于isNotFlying
  • late优于notOneTime

函数调用时避免使用null和undefined参数类型

不要使用undefined或者null的参数作为函数的可选参数。尽量使用可选的Object做参数。尽量使用可选的Object做参数。

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

// later...

const birthdayParty = createEvent({
  title = 'Birthday Party',
  timeStamp = birthDay
});

优于

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

// later...

const birthdayParty = createEvent(
  'Birthday Party',
  undefined, // This was avoidable
  birthDay
);

去掉无用的代码

if语句

if (err) return reject(err);

// do something

优于

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

6、使用平行结构

平行结构需要尽可能相似的结构表达语义。格式上的形似使得读者能够理解不同语句的意义也是相似的。-
William Strunk,Jr., 《英文写作指南》

实际应用中,还有一些额外的问题没有解决。我们可能会重复的做同一件事情。这样的情况出现时,就有了抽象的空间。把相同的部分找出来,并抽象成可以在不同地方同时使用的公共部分。这其实就是很多框架或者功能库做的事情。

以UI控件为例来说。十几年以前,使用jQuery写出把组件、逻辑应用、网络I/O混杂在一起的代码还还很常见。然后人们开始意识到,我们可以在web应用里也使用MVC框架,于是人们逐渐开始把模型从UI更新的逻辑中分离出来。

最终的结构是:web应用使用了组件化模型的方法,这让我们可以用JSX或者HTML模板来构建我们的UI组件。

这就让我们能够通过相同的方式去控制不同组件的更新,而无需对每一个组件的更新写重复的代码。

熟悉组件化的人可以轻易的看到每个组件的工作原理:有一些代码是表示UI元素的声明性标记,也有一些用于事件处理程序和用在生命周期上的回调函数,这些回调函数在需要的时候会被执行。

当我们为相似的问题找到一种模式后,任何熟悉这个模式的人都能很快的理解这样的代码。

使用主动语态

三元判断语句

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

优于

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

结论:代码要简洁,但不是简单化。

刚健的文字是简练的。一句话应该不包含无用的词语,一段话没有无用的句子,正如作画不应该有多余的线条,一个机器没有多余的零件。这就要求作者尽量用短句子,避免罗列所有细节,在大纲里就列出主题,而不是什么都说。-William
Strunk,Jr.,《英文写作指南》

ES6在2015年是标准化的,但在2017年,许多开发人员避免了简洁的箭头功能,隐式回报,休息和传播操作等的功能。人们以编写更容易阅读的代码为借口,但只是因为人们更熟悉旧的模式而已。这是个巨大的错误。熟悉来自于实践,熟悉ES6中的简洁功能明显优于ES5的原因显而易见:相比厚重的语法功能的代码,这样的代码更简洁。

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

简洁的代码就是:

  • 更少的bug
  • 更加便于调试

bug通常是这样的:

  • 修理起来耗时耗力
  • 可能引入更多的bug
  • 打乱正常的工作流程

所以简洁的代码应该要:

  • 易写
  • 易读
  • 易维护

让开发者学会并使用新技术比如curry其实是值得的。这样做也是在让读者们熟悉新知识。如果我们还是依然用原来的做法,这也是对阅读代码人的不尊重,就好像在用成年人在和婴儿讲话时使用孩子的口吻一样。

我们可以假设读者不理解这段代码的实现,但请不要假设阅读代码的人都很笨,或者假设他们连这门语言都不懂。

代码应该简洁,而但不要掉价。掉价才是一种浪费和侮辱。要在实践中练习,投入精力去熟悉、学习一种新的编程语法、一种更有活力的风格。

代码应该简洁,而非简单化。

避免一连串松散结构的代码

恰当的使用否定

有时候我们只关心一个变量是否缺失,如果通过判断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
  • 打断了正常开发的流程

简洁的代码同样:

  • 更易写
  • 更易读
  • 更好去维护

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

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

1、让函数成为代码的基本单元。每个函数做一件事。

软件开发的本质就是写作。我们把模块、函数、数据结构组合在一起,就有了一个软件程序。

理解如何编写函数并如何构建它们,是软件开发者的基本技能。

模块是一个或多个函数或数据结构的简单集合,数据结构是我们如何表示程序的状态,但在没有应用函数,数据结构自身不会发生什么有趣的事情。

JavaScript有三种类型的函数:

交流型函数:执行I/O的函数

功能型函数:一系列指令的合集

映射型函数:给一些输入,返回相应的输出

所有有用的程序都需要I /
O,并且许多程序遵循一些程序顺序,但大多数函数应该像映射函数:给定一些输入,该函数将返回一些相应的输出。

一个函数做一件事:如果你的函数是I/O敏感,那么就不要把I/O和映射混杂在一起。如果你的函数是为了映射,那么就不要加入I/O。功能性的函数就违背了这条准则。功能性的函数还违背了另一条准则:避免把松散的句子写在一起。

理想的函数应该是一个简单的,确定的,纯粹功能函数。

给定相同的输入,返回相同的输出

没有副作用

  1. 去掉无用代码

简练的代码在软件中也很重要,这是因为更多的代码让bug有了藏匿的空间。更少的代码=更少的含有bug的空间=更少bug。

简练的代码更清晰,是因为它有更高的信噪比:读者可以减少对的语法理解更多的了解它的意义。更少的代码=更少的语法噪音=更多信息的传递。

图片 4

上面一段代码可以简化为:

constsecret = msg => () => msg;

对于熟悉箭头函数(ES
2015年加入的新特性)的人来说,这段代码可读性增强了。它去掉了多余的语法:括号,function关键词,以及return返回值语句。

第一个版本包含了不必要的语法。对于熟悉箭头语法的人来说,括号,function关键词,和return语句都没有任何意义。它们存在只是因为还有很多人对ES6的新特性不熟悉。

ES6从2015年就是语言标准了。你应该熟悉它了。

去掉无用的变量

有时候我们倾向给一些实际不需要命名的变量命名。原因是人脑在可用的容量内只能存储有限的资源,并且每个变量都必须作为离散量子存储,占据了我们可用的不多的记忆空间。

因为这个原因,有经验的开发者都倾向减少不需要的变量命名。

比如,在大多数情况下,你应该去掉变量,只给创建一个返回值的变量。函数名应该能够提供足够多的信息以显示它的返回值。看下面的例子:

图片 5

以及:

图片 6

开发者常常用来减少变量的另一个做法是:利用函数组合以及Point-free
的风格。

Point-free
风格是指:定义函数时无需引用对其操作的参数。常用的point-free风格方式主要是curry和函数组合。

看一个使用curry的例子:

图片 7

现在看一下inc()函数。注意它并没有是有function关键词,或者=>语法。没有参数列表,因为这个函数内部并没有使用参数列表。相反的,它返回的是如何处理参数的一个函数。

下面我们看一下使用函数组合的例子。函数组合是把一个函数结果应用到另一个函数的处理流程。你可能没有意识到,你其实一直都在用函数组合。当你调用.map()或者promise.then()函数的时候,你就在使用它了。例如,它的大部分时候的基本形态,其实都像这样:f.在代数中,这样的组合被写成:f
∘ g, 被称作“g后f”或者“f组合g”。

当你把两个函数组合在一起时,你就去掉了需要存储的中间返回值的变量。我们看一下下面这个可以更简单的代码:

图片 8

使用仿函数也能实现类似的效果。使用仿函数也能实现类似的效果。下面这段代码就是使用仿函数的一个例子:

图片 9

其实在你使用promise链时,基本上就是在用这个方法了。

实际上, 每个编程序库都至少有两个版本的实用方法:compose
()把函数从右向左组合,pipe()函数将函数从左向右组合。

Lodash把这两个函数称作compose。当我在Lodash里使用它们时,一般都这样引入:

图片 10

然而,下面的代码更少,而且完成的了同样的事情

图片 11

如果函数组合对你来说像外星人一样深不可测,而且你也不确定如何使用,那么请认真回顾一下前面的话:

软件开发的本质是写作。我们把模块、函数、数据结构组合在一起,就构成了软件程序。

由此你就可以得出结论:理解函数的工具意义和对象组合,就像是一个家庭手工劳动者要能理解如何使用钻子和钉子枪一样的基本技能。

当你用指令集和中间变量把不同函数组合在一起时,其实就像是用胶布和疯狂的胶水随意的把东西沾在一起。

请记住:

如果能用更少的代码表达相同的意思,且不改变或混淆代码含义,那就应该这样做。

如果可以使用更少变量达到相同目的,也不会改变或混淆原意,那也应该这样做。

3.使用主动语态

主动语态比被动语态更加直接、有力。 — William Strunk,Jr. 《英文写作指南》

命名越直接越好。

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

createUser()优于User.create()。

notify()优于Notifier.doNotification()。

命名断言或者布尔变量时尽量使用是或否的问题形式:

isActive优于getActiveStatus。

isFirstRun = false;优于firstRun = false;。

命名函数使用动词形式

increment()优于plusOne()。

unzip()优于filesFromZip()。

filter(fn, array)优于matchingItemsFromArray。

事件处理

事件处理函数和生命周期的函数是个例外,要避免使用动词形式,因为他们通常是为了说明这时该做什么而不是他们作为主语自身要做了什么。功能应该和命名一致。

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

component.onDragStart(handleDragStart)优于component.startDrag(handleDragStart)。

这个例子里两种命名方法的第二种,看上去更像是我们尝试触发一件事,而不是对这个事件作出响应。

生命周期函数

假设有一个组件,有这样一个生命周期函数,在它更新之前要调用一个事件处理的函数,有以下几种命名方式:

componentWillBeUpdated(doSomething)

componentWillUpdate(doSomething)

componentWillUpdate(doSomething)

第一种命名使用被动语态。这种方式有点绕口,不如其他方式直观。

第二种方式稍好,但是给人的意思是这个生命周期方法要调用一个函数。componentWillUpdate读起来就像是这个组件要更新一个事件处理程序,这就偏离了本意。我们的原意是:”在组件更新前,调用事件处理”beforeComponentUpdate()这样命名更为恰当清晰。

还能更精简。既然这些都是方法,那么主语其实已经确定了。调用这个方法时再带有主语就重复了。想象一下看到这段代码时,你会看到component.componentWillUpdate()。这就像是在说“吉米,吉米中午要吃牛排”。你其实不需要听到重复的名字。

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

Functional mixins
是把属性和方法添加到Object对象上的一种方法。函数一个接一个的组合添加在一起,就像是管道流一样,或者像组装线一样。每个functional
mixin的函数都有一个instance作为输入,把一些额外的东西附加上去,然后再传递给下一个函数,就像组装流水线一样。

我倾向用形容词命名mixin
函数。你也可以使用“ing”或者“able”之类的后缀来表示形容词的含义。例如:

const duck = composeMixins(flying,quacking);

const box = composeMixins(iterable,mappable);

4、避免一连串松散的语句

开发者其实常常讲一连串的事件连接成一整个处理过程:一系列松散的语句本来就为了一个接一个而设计存在的。但过度使用这样的流程会导致代码像意大利面一样错综复杂。

这种序列常常被重复,尽管会有些许的不同,有时还会出乎意料的偏离正规。例如,一个用户界面可能会和另外的用户界面共享了同样的组件代码。这样的代价就是代码可能被分到不同的生命周期里并且一个组件可能由多个不同的代码块进行管理。

参考下面这个例子:

constdrawUserProfile = ({ userId }) => {constuserData =
loadUserData;constdataToDisplay = calculateDisplayData;
renderProfileData(dataToDisplay);};

这段代码做了三件事:加载数据,计算相关状态,然后渲染内容。

在现代的前端应用框架中,这三件事是相互分离的。通过分离,每件事都可以得到比较好的组合或者扩展。

例如,我们可以完全替换渲染器,而不用影响其他部分;例如,React有丰富的自定义渲染器:适用于原生iOS和Android应用程序的ReactNative,WebVR的AFrame,用于服务器端渲染的ReactDOM
/ Server 等等。

另一个问题是你无法简单的计算要显示的数据并且如果没有第一次加载数据就无法生成显示页面。假如你已经加载了数据呢?那么你的计算逻辑就在接下来的调用中变的多余了。

分离也使得各个部件独立可测。我喜欢给自己的应用加很多单元测试,并且把测试结果显示出来,这样我有任何改动的时候都能看到。但是,如果我要尝试测试加载数据并渲染的功能,那我就不能只用一些假数据测试渲染部分。正在保存……

我无法通过单元测试立刻获得结果。函数分离却可以让我们能够进行独立的测试。

这个例子就已经说明,分离函数可以让我们能够参与到应用的不同生命周期中去。可以在应用加载组件后,触发数据的加载功能。计算和渲染可以在视图发生变化的时候进行。

这样的结果就是更清楚地描述了软件的责任:可以重用组件相同的结构以及生命周期的回调函数,性能也更好;在后面工作流程中,我们也节省了不必要的劳动。

5.把相关的代码放在一起。

很多框架或者样板程序都预设了一种程序的组织方法,那就是按照文件类型划分。如果你做一个小的计算器或者To
Do的应用,这样做没问题;但是如果是大型项目,更好的办法是按功能对文件进行分组。

下面以一个To Do 应用为例,有两种文件组织结构。

按照文件类型分类

图片 12

按照文件功能分类

图片 13

按照功能组织文件,可以有效避免在文件夹视图中不断的上下滚动,直接去到功能文件夹就可以找到要编辑的文件了。

把文件按照功能进行组织。

6.陈述句和表达式使用主动语态。

“做出明确的断言。避免无聊、不出彩、犹豫、不可置否的语气。使用“not”时应该表达否定或者对立面的意思,而不要用来作为逃避的手段。”William
Strunk,Jr., 《英文写作指南》。

isFlying优于isNotFlying。

late优于notOnTime。

If语句

ifreturnreject;//dosomething…

比下面这种方式更好:

if {//…dosomething}else{returnreject;}

三元表达式

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

比下面的形式更好:

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

尽量选择语气强烈的否定句

有时候我们只关系一个变量是否缺失,因此使用主动语法会让我们被迫加上一个!。在这些情况下,不如使用语气强烈的否定句式。“not”这个词和!的语气相对较弱。

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

if (anonymous)优于if 。

if (isEmpty优于if (notDefined。

函数调用时避免使用null和undefined参数类型

不要使用undefined或者null的参数作为函数的可选参数。尽量使用可选的Object做参数。尽量使用可选的Object做参数。

图片 14

优于

图片 15

6、使用平行结构

实际应用中,还有一些额外的问题没有解决。我们可能会重复的做同一件事情。这样的情况出现时,就有了抽象的空间。把相同的部分找出来,并抽象成可以在不同地方同时使用的公共部分。这其实就是很多框架或者功能库做的事情。

以UI控件为例来说。十几年以前,使用jQuery写出把组件、逻辑应用、网络I/O混杂在一起的代码还还很常见。然后人们开始意识到,我们可以在web应用里也使用MVC框架,于是人们逐渐开始把模型从UI更新的逻辑中分离出来。

最终的结构是:web应用使用了组件化模型的方法,这让我们可以用JSX或者HTML模板来构建我们的UI组件。

这就让我们能够通过相同的方式去控制不同组件的更新,而无需对每一个组件的更新写重复的代码。

熟悉组件化的人可以轻易的看到每个组件的工作原理:有一些代码是表示UI元素的声明性标记,也有一些用于事件处理程序和用在生命周期上的回调函数,这些回调函数在需要的时候会被执行。

当我们为相似的问题找到一种模式后,任何熟悉这个模式的人都能很快的理解这样的代码。

结论:代码要简洁,但不是简单化。

刚健的文字是简练的。一句话应该不包含无用的词语,一段话没有无用的句子,正如作画不应该有多余的线条,一个机器没有多余的零件。这就要求作者尽量用短句子,避免罗列所有细节,在大纲里就列出主题,而不是什么都说。-William
Strunk,Jr.,《英文写作指南》

ES6在2015年是标准化的,但在2017年,许多开发人员避免了简洁的箭头功能,隐式回报,休息和传播操作等的功能。人们以编写更容易阅读的代码为借口,但只是因为人们更熟悉旧的模式而已。这是个巨大的错误。熟悉来自于实践,熟悉ES6中的简洁功能明显优于ES5的原因显而易见:相比厚重的语法功能的代码,这样的代码更简洁。

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

简洁的代码就是:

更少的bug

更加便于调试

bug通常是这样的:

修理起来耗时耗力

可能引入更多的bug

打乱正常的工作流程

所以简洁的代码应该要:

易写

易读

易维护

让开发者学会并使用新技术比如curry其实是值得的。这样做也是在让读者们熟悉新知识。如果我们还是依然用原来的做法,这也是对阅读代码人的不尊重,就好像在用成年人在和婴儿讲话时使用孩子的口吻一样。

我们可以假设读者不理解这段代码的实现,但请不要假设阅读代码的人都很笨,或者假设他们连这门语言都不懂。

代码应该简洁,而但不要掉价。掉价才是一种浪费和侮辱。要在实践中练习,投入精力去熟悉、学习一种新的编程语法、一种更有活力的风格。

代码应该简洁,而非简单化。

感谢阅读

喜欢看小编文章的点个订阅或者喜欢!小编每天都会跟大家分享文章,也会给大家提供web前端学习资料。

发表评论

电子邮件地址不会被公开。 必填项已用*标注