JavaScript 避免使用 if-else 的方法

本文最后更新于:2020年12月25日 晚上

场景

在日常编写 JavaScript 代码的过程中,或许会遇到一个很常见的问题。根据某个状态,进行判断,并执行不同的操作。吾辈并不是说 if-else 不好,简单的逻辑判断 if-else 毫无疑问是个不错的选择。然而在很多时候似乎我们习惯了使用 if-else,导致代码不断庞大的同时复杂度越来越高,所有的 JavaScript 代码都乱作一团,后期维护时越发困难。

GitHub, 演示地址

例如下面这段代码,点击不同的按钮,显示不同的面板。

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
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>JavaScript 避免使用 if-else</title>
</head>
<body>
<main>
<div id="tab">
<label>
<input type="radio" data-index="1" name="form-tab-radio" />
第一个选项卡
</label>
<label>
<input type="radio" data-index="2" name="form-tab-radio" />
第二个选项卡
</label>
<label>
<input type="radio" data-index="3" name="form-tab-radio" />
第三个选项卡
</label>
</div>
<form id="extends-form"></form>
</main>
<script src="./js/if-else.js"></script>
</body>
</html>
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
// js/if-else.js
document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach((el) => {
el.addEventListener("click", () => {
const index = el.dataset.index;
const header = el.parentElement.innerText.trim();
// 如果为 1 就添加一个文本表单
if (index === "1") {
document.querySelector("#extends-form").innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
} else if (index === "2") {
document.querySelector("#extends-form").innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
function readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (event) => {
resolve(event.target.result);
};
fr.onerror = (error) => {
reject(error);
};
fr.readAsDataURL(file);
});
}
document.querySelector("#avatar").addEventListener("change", (evnet) => {
const file = evnet.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("image")) {
return;
}
readLocalFile(file).then((link) => {
document.querySelector("#avatar-preview").src = link;
});
});
} else if (index === "3") {
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`);
document.querySelector("#extends-form").innerHTML = `
<header><h2>${header}</h2></header>
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result"></ul>
</div>
`;
document
.querySelector("#search-text")
.addEventListener("input", (evnet) => {
const searchText = event.target.value;
document.querySelector("#search-result").innerHTML = initData
.filter((v) => v.includes(searchText))
.map((v) => `<li>${v}</li>`)
.join();
});
}
});
});

那么,我们可以如何优化呢?

抽取函数

稍有些经验的 developer 都知道,如果一个函数过于冗长,那么就应该将之分离成多个单独的函数。

所以,我们的代码变成了下面这样

实现思路

  1. 抽取每个状态对应执行的函数
  2. 根据状态使用 if-else/switch 判断然后调用不同的函数
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
// 抽取函数

function switchFirst(header) {
document.querySelector("#extends-form").innerHTML = `
${header}
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
}

function switchSecond(header) {
document.querySelector("#extends-form").innerHTML = `
${header}
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
function readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (event) => {
resolve(event.target.result);
};
fr.onerror = (error) => {
reject(error);
};
fr.readAsDataURL(file);
});
}
document.querySelector("#avatar").addEventListener("change", (evnet) => {
const file = evnet.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("image")) {
return;
}
readLocalFile(file).then((link) => {
document.querySelector("#avatar-preview").src = link;
});
});
}

function switchThree(header) {
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`);
document.querySelector("#extends-form").innerHTML = `
${header}
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result"></ul>
</div>
`;
document.querySelector("#search-text").addEventListener("input", (evnet) => {
const searchText = event.target.value;
document.querySelector("#search-result").innerHTML = initData
.filter((v) => v.includes(searchText))
.map((v) => `<li>${v}</li>`)
.join();
});
}

function switchTab(el) {
const index = el.dataset.index;
const header = `<header><h2>${el.parentElement.innerText.trim()}</h2></header>`;
// 如果为 1 就添加一个文本表单
if (index === "1") {
switchFirst(header);
} else if (index === "2") {
switchSecond(header);
} else if (index === "3") {
switchThree(header);
}
}

document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach((el) => {
el.addEventListener("click", () => switchTab(el));
});

ES6 class:有限状态机

如果你知道 ES6 的 class 的话,应该也了解到目前 js 可以使用 class 模拟面向对象的继承,以及多态。

实现思路

  1. 创建一个基类,并在其中声明一个需要被子类重写的方法
  2. 根据不同的状态创建不同的子类,并分别实现基类的方法
  3. 添加一个 Builder 类,用于根据不同的状态判断来创建不同的子类
  4. 调用者使用 Builder 类构造出来的对象调用父类中声明的方法

具体实现

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 有限状态机

class Tab {
// 基类里面的初始化方法放一些通用的操作
init(header) {
const html = `
<header><h2>${header}</h2></header>
${this.initHTML()}
`;
document.querySelector("#extends-form").innerHTML = html;
}

// 给出一个方法让子类实现,以获得不同的 HTML 内容
initHTML() {}
}

class Tab1 extends Tab {
// 实现 initHTML,获得选项卡对应的 HTML
initHTML() {
return `
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
}
}

class Tab2 extends Tab {
initHTML() {
return `
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
}
// 重写 init 初始化方法,并首先调用基类通用初始化的方法
init(header) {
super.init(header);
document.querySelector("#avatar").addEventListener("change", (evnet) => {
const file = evnet.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("image")) {
return;
}
this.readLocalFile(file).then((link) => {
document.querySelector("#avatar-preview").src = link;
});
});
}
// 子类独有方法
readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (event) => {
resolve(event.target.result);
};
fr.onerror = (error) => {
reject(error);
};
fr.readAsDataURL(file);
});
}
}

class Tab3 extends Tab {
initHTML() {
return `
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result" />
</div>
`;
}
init(header) {
super.init(header);
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`);
document
.querySelector("#search-text")
.addEventListener("input", (evnet) => {
const searchText = event.target.value;
document.querySelector("#search-result").innerHTML = initData
.filter((v) => v.includes(searchText))
.map((v) => `<li>${v}</li>`)
.join();
});
}
}

class TabBuilder {
/**
* 获取一个标签子类对象
* @param {Number} index 索引
* @returns {Tab} 子类对象
*/
static getInstance(index) {
// Tab 构造类,用于根据不同的状态 index 构造不同的 Tab 对象
const tabMap = new Map(
Object.entries({
1: () => new Tab1(),
2: () => new Tab2(),
3: () => new Tab3(),
})
);
return tabMap.get(index)();
}
}

document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach((el) => {
el.addEventListener("click", () =>
// 首先通过 Builder 构造类获取 Tab 子类实例,然后调用初始化方法 init
TabBuilder.getInstance(el.dataset.index).init(
el.parentElement.innerText.trim()
)
);
});

主要优势

  • 分离了状态与执行函数之间的关联,具体执行由具体的子类决定
  • 子类允许包含独有的属性/方法
  • 可扩展性更好,随时可以扩展任意多个子类

ES6 class:无限状态机

上面使用 class 继承多态实现的状态机虽然很好,但却并不能应对 不确定 具体有多少种状态的情况。因为每个子类都与父类有着强关联,直接在 Builder 类中进行了声明。那么,有没有一种方式,可以在添加/删除后不影响基类或者构造类呢?

  1. 创建一个基类,并在其中声明一个需要被子类重写的方法
  2. 添加一个 Builder 类,具体子类对应的状态由子类的某个属性决定
  3. 根据不同的状态创建不同的子类,并分别实现基类的方法,调用 Builder 类的方法注册自身

    此处因为 js 无法通过反射拿到所有子类,所以子类需要在 Builder 类注册自己

  4. 使用 Builder 构造子类对象,并调用基类声明的方法

具体实现

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// 无限状态机

class Tab {
// 基类里面的初始化方法放一些通用的操作
init(header) {
const html = `
<header><h2>${header}</h2></header>
${this.initHTML()}
`;
document.querySelector("#extends-form").innerHTML = html;
}

// 给出一个方法让子类实现,以获得不同的 HTML 内容
initHTML() {}
}

/**
* 状态机
* 用于避免使用 if-else 的一种方式
*/
class StateMachine {
static getBuilder() {
const clazzMap = new Map();
/**
* 状态注册器
* 更好的有限状态机,分离子类与构建的关系,无论子类如何增删该都不影响基类及工厂类
*/
return new (class Builder {
// noinspection JSMethodCanBeStatic
/**
* 注册一个 class,创建子类时调用,用于记录每一个 [状态 => 子类] 对应
* @param state 作为键的状态
* @param clazz 对应的子类型
* @returns {*} 返回 clazz 本身
*/
register(state, clazz) {
clazzMap.set(state, clazz);
return clazz;
}

// noinspection JSMethodCanBeStatic
/**
* 获取一个标签子类对象
* @param {Number} state 状态索引
* @returns {QuestionType} 子类对象
*/
getInstance(state) {
const clazz = clazzMap.get(state);
if (!clazz) {
return null;
}
//构造函数的参数
return new clazz(...Array.from(arguments).slice(1));
}
})();
}
}
/**
* 状态注册器
* 更好的有限状态机,分离子类与构建的关系,无论子类如何增删该都不影响基类及工厂类
*/
const builder = StateMachine.getBuilder();

const Tab1 = builder.register(
1,
class Tab1 extends Tab {
// 实现 initHTML,获得选项卡对应的 HTML
initHTML() {
return `
<div>
<label for="name">姓名</label>
<input type="text" name="name" id="name" />
</div>
<div>
<label for="age">年龄</label>
<input type="number" name="age" id="age" />
</div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
}
}
);

const Tab2 = builder.register(
2,
class Tab2 extends Tab {
initHTML() {
return `
<div>
<label for="avatar">头像</label>
<input type="file" name="avatar" id="avatar" />
</div>
<div><img id="avatar-preview" src="" /></div>
<div>
<button type="submit">提交</button> <button type="reset">重置</button>
</div>
`;
}
// 重写 init 初始化方法,并首先调用基类通用初始化的方法
init(header) {
super.init(header);
document.querySelector("#avatar").addEventListener("change", (evnet) => {
const file = evnet.target.files[0];
if (!file) {
return;
}
if (!file.type.includes("image")) {
return;
}
this.readLocalFile(file).then((link) => {
document.querySelector("#avatar-preview").src = link;
});
});
}
// 子类独有方法
readLocalFile(file) {
return new Promise((resolve, reject) => {
const fr = new FileReader();
fr.onload = (event) => {
resolve(event.target.result);
};
fr.onerror = (error) => {
reject(error);
};
fr.readAsDataURL(file);
});
}
}
);

const Tab3 = builder.register(
3,
class Tab3 extends Tab {
initHTML() {
return `
<div>
<label for="search-text">搜索文本</label>
<input type="text" name="search-text" id="search-text" />
<ul id="search-result" />
</div>
`;
}
init(header) {
super.init(header);
const initData = new Array(100).fill(0).map((v, i) => `第 ${i} 项内容`);
document
.querySelector("#search-text")
.addEventListener("input", (evnet) => {
const searchText = event.target.value;
document.querySelector("#search-result").innerHTML = initData
.filter((v) => v.includes(searchText))
.map((v) => `<li>${v}</li>`)
.join();
});
}
}
);

document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach((el) => {
el.addEventListener("click", () =>
// 调用方式不变
builder
.getInstance(Number.parseInt(el.dataset.index))
.init(el.parentElement.innerText.trim())
);
});

主要优势

  • 可扩展性最好,添加/修改/删除子类不影响父类及构造类

那么,关于 JavaScript 中如何避免使用 if-else 到这里就结束啦


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!