no-multi-assign

禁止使用链式赋值表达式,链接变量的赋值可能会导致意外结果并且难以阅读,此规则不允许在单个语句中使用多个赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// incorrect code
var a = b = c = 5;

const foo = bar = "baz";

let a =
b =
c;

class Foo {
a = b = 10;
}

a = b = "quux";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// correct code
var a = 5;
var b = 5;
var c = 5;

const foo = "baz";
const bar = "baz";

let a = c;
let b = c;

class Foo {
a = 10;
b = 10;
}

a = "quux";
b = "quux";

no-this-before-super

在构造函数中调用之前禁止this/supersuper(),在派生类的构造函数中,如果在调用之前使用this/ ,则会引发引用错误。supersuper(),此规则检查构造函数中的this/super关键字,然后报告 之前的关键字super()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// incorrect code
class A extends B {
constructor() {
this.a = 0;
super();
}
}

class A extends B {
constructor() {
this.foo();
super();
}
}

class A extends B {
constructor() {
super.foo();
super();
}
}

class A extends B {
constructor() {
super(this.foo());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// correct code
class A {
constructor() {
this.a = 0; // OK, this class doesn't have an `extends` clause.
}
}

class A extends B {
constructor() {
super();
this.a = 0; // OK, this is after `super()`.
}
}

class A extends B {
foo() {
this.a = 0; // OK. this is not in a constructor.
}
}

no-param-reassign

禁止重新分配function参数
对声明为函数参数的变量进行赋值可能会产生误导并导致令人困惑的行为,因为修改函数参数也会改变对象arguments。通常,对函数参数的赋值是无意的,并且表明存在错误或程序员错误。
该规则也可以配置为在修改函数参数时失败。对参数的副作用可能会导致违反直觉的执行流程,并使错误难以追踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// incorrect code
function foo(bar) {
bar = 13;
}

function foo(bar) {
bar++;
}

function foo(bar) {
for (bar in baz) {}
}

function foo(bar) {
for (bar of baz) {}
}
1
2
3
4
// correct code
function foo(bar) {
var baz = bar;
}

default-param-last

默认参数位于最后。

1
2
3
4
5
6
7
8
9
10
// incorrect code
function f(a = 0, b: number) {}
function f(a: number, b = 0, c: number) {}
function f(a: number, b?: number, c: number) {}
class Foo {
constructor(public a = 10, private b: number) {}
}
class Foo {
constructor(public a?: number, private b: number) {}
}
1
2
3
4
5
6
7
8
9
10
11
12
// correct code
function f(a = 0) {}
function f(a: number, b = 0) {}
function f(a: number, b?: number) {}
function f(a: number, b?: number, c = 0) {}
function f(a: number, b = 0, c?: number) {}
class Foo {
constructor(public a, private b = 0) {}
}
class Foo {
constructor(public a, private b?: number) {}
}

eqeqeq

使用类型安全的相等运算符 === 和 !== 而不是其常规对应运算符 ==和被认为是良好的做法!=。
● [] == false
● [] == ![]
● 3 == “03”
如果其中之一出现在看似无辜的声明中,那么a == b实际问题就很难发现。

1
2
3
4
5
6
7
8
9
10
// incorrect code
a == b
foo == true
bananas != 1
value == undefined
typeof foo == 'undefined'
'hello' != 'world'
0 == 0
true == true
foo == null
1
2
3
4
5
6
7
8
9
10
// correct code
a === b
foo === true
bananas !== 1
value === undefined
typeof foo === 'undefined'
'hello' !== 'world'
0 === 0
true === true
foo === null

no-use-before-define

在定义变量之前禁止使用变量。

1
2
3
4
5
6
// incorrect code
const x = Foo.FOO;

enum Foo {
FOO,
}
1
2
3
4
5
6
7
8
// correct code
function foo() {
return Foo.FOO;
}

enum Foo {
FOO,
}

如何写出优雅的代码

渐进式重构

渐进式重构是不断地对既有代码进行抽象、分离和组合。做代码重构之前需要回答两个问题:
1、什么样的代码需要重构?
2、何时进行重构?

设计不是一蹴而就的,有时候写着写着才发现某些代码可以抽离出来单独使用,需要重构的代码需要满足几个条件:
1、代码后期可复用
2、代码无副作用
3、代码逻辑单一

过早重构可能会因需求变化太快白白浪费许多时间;过晚重构会因为代码逻辑复杂、相似代码积压过多导致变更风险太高,难以维护。渐进式重构如下图所示(红色部分为增加的代码):

首先我们在同一个源文件中新增功能,发现部分代码无副作用且可分离,因此在同一个文件中进行代码分割,形成许多功能单一的模块。如此往复后发现单文件的体积越来越大,此时就可以将功能相关联的模块抽出来放到单独的文件中统一管理,如 helpers、components、constants 等等。

高内聚低耦合

高内聚低耦合一直是软件设计领域里亘古不变的话题,重构的目标是提高代码的内聚性,降低各功能间的耦合程度,降低后期维护成本,特别是写业务代码,这一点相当重要。
举个栗子,比如新需求希望在现有的产品页面上增加发红包功能,以吸引用户开通某个功能,按照正常逻辑,我需要:
1、在当前页面中引入相关依赖
2、初始化,查询红包相关信息
3、用户点击时,触发红包发送

白色部分表示上个版本的代码,红色部分表示完成这个需求需要变更的代码:

这样一来,这个发红包功能就和以前的代码严重耦合,如果这是个只需要上线一周的临时需求,下线代码的时候就是一个高风险的动作;如果上线运行期间还需要对产品页面进行迭代,越往后就越搞不清楚谁是谁了。合理的设计应该是下面这个样子的:

将和产品代码无关的功能性代码拆分出来,放到另一个文件中内部维护好整个生命周期状态,对外只暴露少量的接口或是方法,这样一来对产品页面的改造只需要:
1、引入红包组件
2、用户点击时,调用红包组件的发奖方法

这样的变更是极小的、明确的、可控的。换句话说,整个红包功能是高内聚的,与产品代码是低耦合的。这样实践也带来另一个好处:我得到了一个可复用的红包组件!

合理冗余

业务需求是多变的,写出来的代码也是如此,频繁地抽象很可能导致过度设计,一个抽象很可能随着迭代次数的增多变得十分复杂。在存在多个变量的分支业务场景,比如同时包含活动是否过期、是否已参加活动、是否完成一次任务这样的情况,会存在多个嵌套 if-else 结构,这时将代码冗余设计是个不错的选择。下面举一个例子来说明什么是合理冗余:
e.g. 有这样一个需求,一开始很简单,需要设计两个运营展位:

那么抽象一个组件:

1
2
3
4
5
6
7
const Item = ({ title, content }) => (
<div>
<h4>{title}</h4>
<p>{content}</p>
</div>
);

现在需求要求在第一个展位的标题上增加热文标记:

也很容易:

1
2
3
4
5
6
7
const Item = ({ title, content }, index) => (
<div>
<h4>{title}{index === 0 && <span>hot</span>}</h4>
<p>{content}</p>
</div>
);

需求又变了,要求:在第一个展位去掉内容,并且在下方加个按钮;第二个展位的标题右边增加一个超链接以及增加一个副标题:

这下有点恶心了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Item = ({ title, content }, index) => (
<div>
<h4>
{title}
{index === 0 && <span>hot</span>}
{index === 1 && <a href="xxx">去看看</a>}
</h4>
{index === 1 && <h5>副标题</h5>}
<p>
{index !== 0 && content}
{index === 0 && <button>领福利<button>}
</p>
</div>
);

可以看到,之前抽象的好好的,现在需求一变,代码就面目全非了,中间混杂着两个状态(第一个、第二个)的判断逻辑。实际情况很可能比这个更复杂,在多状态交织逻辑难以通过一套代码表达清楚时,进行合理冗余就是个不错的选择,将上面的例子用两个 if 重写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 第一个展位
if (index === 0) {
return (
<div>
<h4>标题一<span>hot</span></h4>
<p><button>领福利<button></p>
</div>
);
}
// 第二个展位
if (index === 1) {
return (
<div>
<h4>标题二<a href="xxx">去看看</a></h4>
<h5>副标题</h5>
<p>内容</p>
</div>
);
}

合理冗余其实也是一种重构,根据业务逻辑和代码规模,做相似抽象还是代码冗余,这其实也是渐进式重构的一种体现。无论采用何种方式,只要能把业务逻辑表达清楚,让代码始终保持良好的可读性和可维护性,就OK。

下面介绍一个过度抽象的例子。

拒绝过度抽象

在 JavaScript 代码中进行深度抽象有时并非好事,有 OOP(面向对象编程)背景的同学很容易先入为主设计:所有数据结构都想封装成一个类 (Class) 。实际上 Class 在 JavaScript 中是个不好的设计,它并非真正的类。几年前,我曾看到一位 Java 转前端的同学写出了类似这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class DataItem {
constructor(id, name, value) {
this.id = id;
this.name = name;
this.value = value;
}
}

class DataCollection {
constructor() {
this.items = new Array();
}
insert(item) {
this.items.push(item);
}
}

const item1 = new DataItem(1, 'name1', 100);
const item2 = new DataItem(2, 'name2', 200);
const list = new DataCollection();
list.insert(item1);
list.insert(item2);
...

一股浓浓的 Java 味道扑面而来。上面的代码并没有发挥出 JavaScript 的语言优势,也增加了不少理解成本,如果用面向对象编程的思路去写前端代码,特别是业务代码,可真是一场噩梦。正确的写法如下:

1
2
3
4
5
6
7
8
9
const list = [{
id: 1,
name: 'name1',
value: 100
}, {
id: 2,
name: 'name2',
value: 200
}];

由于 JS 属于弱类型语言,弱类型语言就要发挥弱类型的优势,无需过多类型定义和 Class 抽象,用最原始的 object 和 function 足以胜任从简单到复杂的业务场景。这里特别想提及前端所熟知的 Redux 状态管理器,Redux 中,state 就是普通的 object,reducer 就是普通的 function,action 也是普通的 object,不加任何类型约束。因为简单,所以强大。

眼观六路

用弱类型语言编程意味着无需编译,无需编译的语言天生存在一个问题是在运行前缺少必要的类型检查,将问题暴露在运行时往往会导致非常严重的故障。这就要求开发者能在写代码的阶段严格保证代码质量,特别是写业务代码。
集成开发环境(IDE)对 JavaScript 代码的智能提示能力有限,很多时候不能通过 IDE 查找某个变量或者函数的所有引用,这时就要善用 Ctrl + F 进行全局查找来保证自己的单点变更不会影响到其他地方。如果使用 TypeScript,在类型检查、引用查找上的帮助会更好。

总结

今天给大家分享了关于书写业务代码的一些实践经验:对代码进行渐进式重构是提升代码健壮性的有力武器;设计高内聚低耦合的代码可以让你在做需求的过程中沉淀出一套通用解决方案;合理冗余可以简化复杂的场景,让开发变得高效、测试变得容易;拒绝过度抽象,拥抱简单,灵活变化。保持 眼观六路 的好习惯能让代码质量提升一个台阶。
最后,希望大家能在实际开发过程中去体会和学习,不断思考和总结,将业务代码写优雅,是个很大的挑战。

作者:蚂蚁保险体验技术
链接:https://juejin.cn/post/6844903833546702856
来源:掘金