本文最后更新于: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
| <!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
| document.querySelectorAll('#tab input[name="form-tab-radio"]').forEach((el) => { el.addEventListener('click', () => { const index = el.dataset.index const header = el.parentElement.innerText.trim() 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 都知道,如果一个函数过于冗长,那么就应该将之分离成多个单独的函数。
所以,我们的代码变成了下面这样
实现思路
- 抽取每个状态对应执行的函数
- 根据状态使用
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>` 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
模拟面向对象的继承,以及多态。
实现思路
- 创建一个基类,并在其中声明一个需要被子类重写的方法
- 根据不同的状态创建不同的子类,并分别实现基类的方法
- 添加一个
Builder
类,用于根据不同的状态判断来创建不同的子类
- 调用者使用
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 }
initHTML() {} }
class Tab1 extends Tab { 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(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 {
static getInstance(index) { 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', () => TabBuilder.getInstance(el.dataset.index).init( el.parentElement.innerText.trim(), ), ) })
|
主要优势
- 分离了状态与执行函数之间的关联,具体执行由具体的子类决定
- 子类允许包含独有的属性/方法
- 可扩展性更好,随时可以扩展任意多个子类
ES6 class:无限状态机
上面使用 class 继承多态实现的状态机虽然很好,但却并不能应对 不确定 具体有多少种状态的情况。因为每个子类都与父类有着强关联,直接在 Builder 类中进行了声明。那么,有没有一种方式,可以在添加/删除后不影响基类或者构造类呢?
- 创建一个基类,并在其中声明一个需要被子类重写的方法
- 添加一个
Builder
类,具体子类对应的状态由子类的某个属性决定
- 根据不同的状态创建不同的子类,并分别实现基类的方法,调用
Builder
类的方法注册自身
此处因为 js 无法通过反射拿到所有子类,所以子类需要在 Builder
类注册自己
- 使用
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 }
initHTML() {} }
class StateMachine { static getBuilder() { const clazzMap = new Map()
return new (class Builder {
register(state, clazz) { clazzMap.set(state, clazz) return clazz }
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() { 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(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 到这里就结束啦