让你的 JS 代码变得更加优雅且可维护

陈大鱼头 ... 2021-8-3 Js
  • 前端
  • Js
  • 优化
  • 面试
About 8 min

鱼头在开发的过程中,总结了一些优化开发的编码经验,当然这些经验都是前人总结出来的,这次就特别拿出来跟大家一起分享,当然这些经验不一定是最佳实践,各位读者有兴趣或者有不同意见的可以跟鱼头一起探讨一下。

# 拒绝魔法

众所周知,魔法是这样的:

哦,不是。。

在编程的世界里也有魔法,一般称其为:魔法数字,魔法变量,魔法字符串。例如这样:

const a = await abcdefg();
console.log(a === 200);
const b = await asdfgh();
if (b === 0) {
} else if (b === 1) {
} else if (b === 2) {};
for (let i = 0; i < 10; i++) {};
1
2
3
4
5
6
7

以上直接出现的,莫名其妙的变量名,字符串以及判断条件数字,就叫魔法。。。

这种写法写出来的代码晦涩难懂,难以维护,隐藏 BUG 多,除非你准备给接手的人埋坑,或者准备辞职,不然千万别这么写(容易被打断腿,👨‍🦽 )

那么怎么写才更优雅?

# 语义化

首先便是语义化。一个是变量,常量的语义化,例如:

const SUCCESS_STATUS = 200;
const requestStatus = await getStatus();
console.log(requestStatus === SUCCESS_STATUS);
const userRole = await getUserRole();
const GUEST_CODE = 0;
const USER_CODE = 1;
const ADMIN_CODE = 2;
if (userRole === GUEST_CODE) {
} else if (userRole === USER_CODE) {
} else if (userRole === ADMIN_CODE) {};
const MAX_NUM = 10;
const MIN_NUM = 0;
for (let currentNum = MIN_NUM; currentNum < MAX_NUM; currentNum++) {};
1
2
3
4
5
6
7
8
9
10
11
12
13

一般的规则就是变量用小写,常量用大写,把变量名语义化,那么当你看到这段代码的时候,一眼就能知道它是做什么的,而不是非得要浪费时间看完上下文,或者是猜。

# 枚举

对于上面判断 userRole 的代码,其实我们可以用更优雅的方式去实现,那就是 枚举

按照维基百科的说明:在数学和计算机科学理论中,一个集的枚举是列出某些有穷序列集的所有成员的程序,或者是一种特定类型对象的计数。这两种类型经常(但不总是)重叠。

其实就是组织收集有关联变量的一种方式。枚举的好处在于方便多状态的管理,以及可读性更强。例如:

const ROLES = {
    GUEST: 0,
    USER: 1,
    ADMIN: 2
};
const userRole = await getUserRole();
if (userRole === ROLES.GUEST) {
} else if (userRole === ROLES.USER) {
} else if (userRole === ROLES.ADMIN) {};
1
2
3
4
5
6
7
8
9

通过枚举的方式归纳起来,维护起来更方便,而且要添加状态直接在 ROLES 对象里写就行,更方便快捷。

# 策略模式

维基百科上说:策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。

上面的代码依旧是可优化的,在这里我们可以利用策略模式来做进一层的优化。

具体的例子就是如下:

const ROLES = {
    GUEST: 0,
    USER: 1,
    ADMIN: 2
};
const ROLE_METHODS = {
    [ROLES.GUEST]() {},
    [ROLES.USER]() {},
    [ROLES.ADMIN]() {},
};
const userRole = await getUserRole();
ROLE_METHODS[userRole]();
1
2
3
4
5
6
7
8
9
10
11
12

通过上面的写法,我们可以知道,当我们需要增加角色,或者修改角色数字的时候,只需要修改 ROLES 里对应的字段,以及 ROLE_METHODS 里的方法即可,这样我们就可以将可能很冗长的 if...else 代码给抽离出来,颗粒度更细,更好维护。

# 更在状态

除了上面的方式之外,我们还可以利用“ 状态 ”的概念来写代码。在看代码之前,我们先了解下什么是 “有限状态机”。

根据维基百科的解释:有限状态机(英语:finite-state machine,缩写:FSM)又称有限状态自动机(英语:finite-state automation,缩写:FSA),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学计算模型。

例如我们熟悉的 Promise ,它就是在状态集:PENDINFULFILLEDREJECTED 之间单向流转的有限状态机。

状态机的概念跟策略模式类似,实现方式也类似,这里面最大的不同是在于 “语义” 。

策略模式更适合于互不依赖,同时只能存在一个状态的场景,例如:

const= {
    沙县大酒店() {
        吃云吞()
    },
    开封菜() {
        吃汉堡()
    },
    在家() {
        吃外卖()
    }
};
1
2
3
4
5
6
7
8
9
10
11

这里面如果我们肚子饿了,就只能在 沙县大酒店()开封菜()在家() 这几个状态里选。

你不能都吃,当然以下情况除外。。。

如果是状态模式,则会有这种情况:

const 打工人 = {
    起床() {},
    上班() {},
    加班() {},
    下班() {}
};
// 早上6点
打工人.起床();
// 早上9点
打工人.上班();
// 晚上6点
打工人.加班();
// 晚上12点
打工人.下班();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这里的打工人根据不同的时间,进行不同的任务,便是打工人模式,哦不,状态模式。这里的时间就是状态。

我们举个实际的业务例子,就是订单列表页,通常我们的订单可能有这几种状态:

不同的状态展示的 UI 也不同,所以我们以不同的状态划分好模块之后,代码写起来就会清晰很多,我们以 Vue 代码为例:

// contants.js
export const ORDER_STATUS = {
    INIT: 0, // 初始化
    CREATED: 1, // 订单创建
    ARREARAGE: 2, // 待支付
    PURCHASED: 3, // 已购买
    SHIPPED: 4, // 已发货
    COMPLETED: 5 // 已完成
};
1
2
3
4
5
6
7
8
9
// order.vue
<template>
    <div>
        <section v-if="orderIsInit"></section>
        <section v-if="orderIsCreated"></section>
        <section v-if="orderIsArrearage"></section>
        <section v-if="orderIsPurchased"></section>
        <section v-if="orderIsShipped"></section>
        <section v-if="orderIsCompleted"></section>
    </div>
</template>

<script>
    import ORDER_STATUS from './contants';
    import eq from 'lodash';
    
    export default {
        computed: {
            /**
            * @func
            * @name orderIsInit
            * @desc 判断订单是否初始化的状态
            * @returns {string} 判断订单是否初始化的状态
            */
            orderIsInit() {
                return eq(this.orderStatus, ORDER_STATUS.INIT);
            },
            /**
            * @func
            * @name orderIsCreated
            * @desc 判断订单是否已创建的状态
            * @returns {string} 订单是否已创建
            */
            orderIsCreated() {
                return eq(this.orderStatus, ORDER_STATUS.CREATED);
            },
            /**
            * @func
            * @name orderIsArrearage
            * @desc 判断订单是否未付款的状态
            * @returns {string} 订单是否未付款
            */
            orderIsArrearage() {
                return eq(this.orderStatus, ORDER_STATUS.ARREARAGE);
            },
            /**
            * @func
            * @name orderIsPurchased
            * @desc 判断订单是否已购买的状态
            * @returns {string} 订单是否已购买
            */
            orderIsPurchased() {
                return eq(this.orderStatus, ORDER_STATUS.PURCHASED);
            },
            /**
            * @func
            * @name orderIsShipped
            * @desc 判断订单是否已发货的状态
            * @returns {string} 订单是否已发货
            */
            orderIsShipped() {
                return eq(this.orderStatus, ORDER_STATUS.SHIPPED);
            },
            /**
            * @func
            * @name orderIsCompleted
            * @desc 判断订单是否已完成的状态
            * @returns {string} 订单是否已完成
            */
            orderIsCompleted() {
                return eq(this.orderStatus, ORDER_STATUS.COMPLETED);
            },
        },
        data() {
            return {
                orderStatus: ORDER_STATUS.INIT // 订单状态
            }
        },
        methods: {
            /**
            * @func
            * @name getOrderStatus
            * @desc 判断订单状态
            * @returns {string} 返回当前订单状态
            */
            async getOrderStatus() {}
        },
        async created() {
            this.orderStatus = await this.getOrderStatus();
        }
    }
</script>
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

将页面组件按状态划分,实现独立自治,这样子既能防止代码耦合,方便维护 debug,也方便开发者自测,如果需要看不同状态的展示效果,只要手动给 orderStatus 赋值即可,方便快捷。

# 面向切面

按照维基百科的解释:面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。

上面这段文字估计没有什么人看,算了,直接上代码吧

我们看回上面打工人的场景,假定老板想要知道打工人每个状态开始前跟结束前的时间以及做点什么,那么该怎么做呢?这个时候我们不难想到可以直接往状态函数里写代码,例如:

const 打工人 = {
    起床() {
        老板.start();
        打工人.do();
        老板.end();
    },
    上班() {
        老板.start();
        打工人.do();
        老板.end();
    },
    加班() {
        老板.start();
        打工人.do();
        老板.end();
    },
    下班() {
        老板.start();
        打工人.do();
        老板.end();
    }
};
// 早上6点
打工人.起床();
// 早上9点
打工人.上班();
// 晚上6点
打工人.加班();
// 晚上12点
打工人.下班();
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
28
29
30

但是这样打工人一下子就察觉到到了老板在监控他的生活,如果要做到不被人察觉(不影响业务逻辑),那我们既可以采用 AOP 的实现方式。代码如下:

import eq from 'lodash';
const TYPES = {
    FUNCTION: 'function'
}
const 老板监控中的打工人 = new Proxy(打工人, {
    get(target, key, value, receiver) {
        console.log('老板开始看你了~');
      	const res = Reflect.get(target, key, value, receiver);
      	const 打工人任务 = eq(typeof res, TYPES.FUNCTION) ? res() : res;
        console.log('老板开始记你小本本了~');
        return () => 打工人任务;
    }
});
1
2
3
4
5
6
7
8
9
10
11
12
13

所以我们可以看到以下结果:

这样子,我们就可以轻松简单地监控到了打工人每天干的活,而且还不让打工人发现,简直是资本家听了都流泪呀。

# 后记

上面总结的只是诸多编程规范模式其中一小部分,还有许多诸如 S.O.L.I.D 以及其余20几个设计模式本文没有提及到,篇幅有限,敬请原谅。

如果你喜欢探讨技术,或者对本文有任何的意见或建议,非常欢迎加鱼头微信好友一起探讨,当然,鱼头也非常希望能跟你一起聊生活,聊爱好,谈天说地。 鱼头的微信号是:krisChans95 也可以扫码关注公众号,订阅更多精彩内容。 https://bucket.krissarea.com/blog/base/qrcode-all1.png

Last update: June 25, 2023 00:16
Contributors: fish_head , 陈大鱼头