命名
規則:除非在小於 5 行的函式裡,否則不要使用單字命名變數
說明:含義不清晰,不能做到「望文生義」
BadCode
var l = data.length;
GoodCode
// 閉包只有一行程式碼,可以使用單字變數
data.map(d => d.length)
規則:不要使用名詞加數字的命名方法
說明:含義不清晰,不能做到「望文生義」
BadCode
var obj = {};
var obj2 = {...obj, key: 1};
GoodCode
var obj = {};
var objWithKey = {...obj, key: 1};
規則:應該且只有方法和函式以動詞開頭
此處動詞沒有包含時態
變數名應該是名詞或者名詞短語。
例外:
- 回撥函式
- 生命週期函式
- 框架級別函式
- getter/setter
說明:
- 函式的命名需要體現內部工作
- 值變數以動詞命名容易讓人誤解為是一個匿名函式
BadCode
// 以名詞開頭,看不明白什麼有什麼功能
function option() {}
// 時態不對
function updatedTime() {}
GoodCode
function selectOption() {}
function updateTime() {}
規則:避免使用拼音或者縮寫命名。
例外:
- 專有名詞:weixin/POI
- 傳統約定:i/j/k 表示迴圈索引
說明:含義不清晰,不能做到「望文生義」
BadCode
var uo = function updateOrder(){}
var as = [].slice;
var ex = Object.extends;
var nu = number
GoodCode
// weixin/wx 是專有名詞
var weixinUser = {};
var wx = weixin;
// POI 是專有名詞
var poi = {};
規則:名稱長短應與其作用域大小相對應。
例外:專有 API,如 alert
說明:在上層作用域下的程式碼,會被更多函式使用到。其名稱應該儘量長或者通用,以保證能夠搜尋到。
BadCode
// 在全域性變數上定義了一個 item 變數,但是很難從命名上理解其作用是什麼。
window.item = {}
GoodCode
window.primaryProductItem = {};
不要在變數/函式尾部加符號、數字
說明:變數中加符號,往往是為了約定其優先順序或者作用域。符號應該在變數名前面。
BadCode
function getDot_(){}
function privateFn$$ (){}
GoodCode
function _getDot() {}
function $$privateFn() {}
規則:例項名稱要和類名相關
說明:類作為例項的所屬,其名稱表達的含義要一脈相承
BadCode
class Person() {}
var dog = new Person(); // dog is a Person ?
GoodCode
class Person() {}
var jack = new Person();
規則:避免直白的中英文翻譯
說明:粗暴的翻譯,更容易造成誤解,還不如寫拼音
BadCode
// 渲染「頁面頂部的執行人」
// 還是渲染「執行砍頭的人」?
function renderHeadExecutantPeople(){}
GoodCode
function renderHeader() {}
規則:概念的命名要一以貫之
說明:避免通一個概念在不同的程式碼用多種不同的單詞描述。
BadCode
// 遠端請求這個概念,先後用了 get/fetch/query
// 三個名詞去描述
function getUserInfo() {}
function fetchProductInfo() {}
function queryPayment() {}
GoodCode
// 統一用 get 描述
// 閱讀程式碼的人能體會到其中的共性
function getUserInfo() {}
function getProductInfo() {}
function getPayment() {}
規則:別用雙關語
例外:專有詞
說明:雙關語容易引起歧義
BadCode
// 訂單型別,還是排序型別?
var orderType
GoodCode
var sortType
規則:命名需要和實現一致
說明:命名往往是實現的隱喻,如果存在差異則會讓閱讀者看不懂程式碼。
BadCode
// empty 的命名含義和實現截然相反
// (我真的見過這種程式碼)
function getProduct(id) {
axios.delete('/product', {id});
}
GoodCode
function deleteProduct(id) {
axios.delete('/product', {id});
}
規則:對於布林值的命名,需要預設其為「真」
說明:布林變數的名稱中,如果加上 「not」之類的否定詞,則相當於做了一次了邏輯判斷。
BadCode
const notEmpty = !!array.length;
GoodCode
const empty = !array.length;
函式
規則:長度不能超過 20 行
說明:程式碼太長說明做的事情不夠專一,同時也會讓閱讀變得很困難。
規則:Don't repeat yourself
說明:同樣功能的程式碼不要重複三次
規則:每個函式只做一件事情,並做好這件事
說明:程式碼裡的邏輯分支要儘量少,只做一件事情,並且要處理好邊界和異常情況。
規則:儘量減少函式的引數,包括 opitons、config 等引數
說明:函式的輸入越多,往往就代表功能約複雜
規則:註釋出來專案/業務的坑
說明:對於比較奇怪的業務邏輯,或者因為系統、介面原因而寫的比較奇怪的邏輯。要通過註釋標註出來
BadCode
framework.doSomeThing();
framework.reset(); // 閱讀者內心 OS:這裡為啥要做一次 reset?
framework.continueSomeThing();
GoodCode
framework.doSomeThing();
// framework 有個 bug,這裡必須要做一次 rest 附連結: http://github.com/issuse/***
framework.reset();
framework.continueSomeThing();
規則:函式要儘量「純」沒有副作用
說明:純函式比較好測試,邏輯也比較清晰,可以放心的引入和刪除。
BadCode
let status;
function method() {
if (status) { ... }
}
GoodCode
function method(status) {
if (status) { ... }
}
規則:函式最好不要修改引數內的資料
說明:修改引數會導致函式的作用變得不可預測
BadCode
function updateObj(obj, value) {
obj.key = value;
return obj;
}
GoodCode
function updateObj(obj, value) {
return {...obj, key: value};
}
規則:除非是 class 的方法,否則不要訪問 this
說明:this 的指向經常不固定,會導致程式碼難以理解。如果呼叫方不熟悉的話,很容易引起 Bug。
BadCode
function method() {
console.log(this.value);
}
method() // 報錯
var obj = { method, value: 1}
obj.method() // 輸出 1
GoodCode
function method(value) {
console.log(value);
}
規則:處理錯誤
說明:錯誤也是一種邏輯分支,如果不處理的話,程式碼就不夠健壯。前端程式碼處理錯誤的方式一般為提示使用者有異常發生。如果錯誤不影響業務流程,則寫入日誌裡並上報。
BadCode
function method(data) {
try { return JSON.parse(data) }
catch (e) {}
}
GoodCode
function method(data) {
try { return JSON.parse(data) }
catch (e) {
alert('資料處理失敗')
}
}
資料
規則:不要有 Magic Number
說明:magic number 是指直接在程式碼中硬編碼的數字,往往具有一些業務含義。
這樣會導致:
- 數字的意義難以理解
- 數值要改動時,要改很多地方
BadCode
if (status === 1) {
...
} else if (type === 4) {
...
}
GoodCode
enum Status {
Closed
}
enum Type {
Array
}
if (status === Status.Closed) {
...
} else if (type === Type.Array) {
...
}
規則:不管是 react state 還是 vue data 存放的業務資料都要具備原子性。
說明:原子性意味著獨立,且不可分割。其它屬性都由原子業務屬性推導、計算而來,這樣能保證狀態的一致。
BadCode
// 當 status 為 open 的時候展示彈窗
// 其它狀態則隱藏彈窗
{
data() {
return {
showAlert: false,
status: 'closed',
}
},
onStatusChange() {
if (status === 'open') {
this.showAlert = true;
} else {
this.showAlert = false;
}
}
}
GoodCode
// showAlert 為非原子的狀態
// 其狀態可以由 status 推導而來
{
data() {
return {
status: 'closed',
}
},
computed: {
showAlert() {
return this.status === 'open';
}
}
}
規則:對於 react state 和 vue data,應當區分業務狀態和 UI 狀態
說明:
- 狀態和 UI 儲存在一起,有時候傳給後端的資料裡會夾雜著沒有必要的 UI 狀態。
- 業務程式碼和 UI 程式碼耦合在一起,業務程式碼沒法複用。
BadCode
// 在一個列表中,使用者可以對資料做多選
// 然後刪除他們
class extends React.Component {
async componentDidMount() {
const listData = getData();
this.setState({ listData })
}
check = (item) => {
const listData = this.state.listData.map(i => {
if (i === item) {
return {...item, checked: true}
}
return i;
});
this.setState({ listData });
}
delete() {
// 返回給後端的資料結構,會多出一個 checked 欄位
deleteItems(this.state.listData.filter(i => i.checked));
}
render() {
const list = this.state.listData.map(item => {
const className = ['item'];
if (item.checked) className.push('active');
return <label
className={className}
onClick={() => this.check(item)}
>{item.naem}</label>;
});
return <>
{list}
<button onClick={this.delete}>delete</button>
</>
}
}
GoodCode
// 在一個列表中,使用者可以對資料做多選
// 然後刪除他們
class extends React.Component {
async componentDidMount() {
const listData = getData();
// 使用獨立的 selected 來儲存 UI 狀態
this.setState({ listData, selected: [] })
}
check = (item) => {
let { selected } = this.state;
selected = selected.findOrInsert(s => s.id, item);
this.setState({ selected });
}
delete() {
const { selected, listData } = this.state;
deleteItems(listData.filter(i => selected.includes(i.id))));
}
render() {
const { selected, listData } = this.state;
const list = listData.map(item => {
const className = ['item'];
if (selected.includes(item.id)) className.push('active');
return <label
className={className}
onClick={() => this.check(item)}
>{item.naem}</label>;
});
return <>
{list}
<button onClick={this.delete}>delete</button>
</>
}
}
規則:對於 react 應用,避免在 render 的時候修改狀態
說明:react 的 render 應該是純函式,在 render 裡執行 setState 會導致重複渲染,或者死迴圈。
BadCode
// 如果 type 為 http 的話,則自動轉換為 https
class extends React.Component {
render() {
const { type } = this.state;
if (type === 'http') {
this.setState({ type: 'https'})
}
return <label>{type}</label>;
}
}
GoodCode
// 如果 type 為 http 的話,則自動轉換為 https
class extends React.Component {
get type() {
const { type } = this.state;
if (type === 'http') return 'https';
return type;
}
render() {
const type = this.type;
return <label>{type}</label>;
}
}
規則:對於雙向繫結應用,避免資料迴圈依賴。
說明:
- 迴圈依賴輕則導致頁面相應慢,重則導致出現髒資料。
- 避免迴圈依賴的前提是理清業務邏輯,搞清楚資料之間的依賴關係。
- 迴圈依賴也是雙向繫結技術的詬病之一。
BadCode
// foo 和 bar 互相依賴,導致了死迴圈
{
data() {
return {
foo: 1,
};
},
computed: {
bar() {
return this.foo + 1;
}
},
watch() {
bar() {
this.foo = this.bar + 1;
},
}
}
規則:訪問資料時,需要考慮邊界情況和 JS 弱型別的特性。
說明:比如用雙等號做判斷
BadCode
const foo = '0';
const bar = 0
// 做資料判斷時不能用雙等號
foo == bar // true
foo ? 1 : 2 // 1
bar ? 1 : 2 // 2
// 僅通過變數有沒有 length 來判斷是否為陣列
if(obj.length) {
obj.forEach(...)
}
GoodCode
const foo = '0';
const bar = 0
foo === bar // false
if (Array.isArray(obj)) {
obj.forEach(...)
}
規則:不要在遍歷陣列的同時,改變陣列資料
說明:這樣做會導致資料的異常。如果需要做這種操作,最好使用陣列函式,或者操作拷貝資料。
BadCode
const array = [1,2,3,4,5,6,7,8,9,10];
// 刪除陣列中的偶數
for (var i = 0; i < array.length; i++) {
if (array[i] % 2 == 0) array.splice(i);
}
// array 變成了 [1]
GoodCode
const array = [1,2,3,4,5,6,7,8,9,10];
array.filter(a => !(a % 2))
API
規則:對 setTimeout 呼叫時,傳遞的時間引數必須有意義。
說明:
大多數場景下,setTimeout 後面傳遞一個時間是為了先執行後續的 A 程式碼,再延後執行程式碼閉包裡的 B 程式碼,如右邊示例程式碼。
但如果隨著業務迭代,A 被改成非同步,或者執行時間很長的話。之前做的延遲執行的防禦措施就時效了,也許反而 B 會比 A 先執行。
BadCode
// 程式碼的本來意圖是讓 B 延後執行
setTimeout(() => {
B();
}, 1000);
A();
// 程式碼的意圖是讓 render 在下一幀執行
// 但是不同裝置,一幀時間是不固定的
setTimeout(() => {
render()
}, 16);
GoodCode
// A 函式內要想辦法使用 Promise 串接起來
await A();
b();
// 使用系統提供的 API 執行動畫幀
requestAnimationFrame(() => {
render();
});
規則:不要使用陳舊的 API
說明:陳舊的 API 往往有很多問題,比如安全、效能、不易讀等。
BadCode
// 判斷是否為陣列
Object.prototype.toString.call(array) === "[object Array]"
// 查詢陣列裡第一個偶數
for (var i = 0; i < array.length; i++) {
if (array[i] % 2 === 0) return array[i];
}
// 遍歷物件的 key
for (var key in obj) {
console.log(key);
}
// 判斷字串/陣列是否包含
'some text'.indexOf('some') >= 0
// 去除首位空格
' some text '.replace(/(^\s+|\s+$)/g, '')
// 新建物件/陣列
const array = new Array();
const obj = new Object();
GoodCode
Array.isArray(array)
array.find(a => a % 2 === 0);
Object.keys(obj).forEach(console.log)
'some text'.includes('some')
' some text '.trim()
const array = [];
const obj = {};
規則:對於 99.9% 的場景,你都不需要使用 React ref
說明:
React Ref 一般是用來處理和原生 DOM 互動的場景,比如 canvas。
大部分對於 React ref 的使用都是錯誤的,大多都拿來用來控制子元素。這種場景我們更推薦用資料流(redux,mobx)或者用狀態提升去做。
React 官方有對「狀態提升」的描述 https://react.docschina.org/d...
BadCode
class List extends React.Component {
async refresh() {
this.setState({
items: getItems(),
});
}
render() {
return this.state.items.map(i => <label>{i}</label>);
}
}
class extends React.Component {
onRefresh = () => {
// 用 ref 去呼叫子元素的方法
this.list.refresh();
}
render() {
return <>
<List ref={l => this.list = l}></List>
<button onClick={this.onRefresh}/>
</>;
}
}
GoodCode
class List extends React.Component {
render() {
return this.props.items.map(i => <label>{i}</label>);
}
}
class extends React.Component {
// 把資料狀態提升到父元件做操作
refresh = async () => {
this.setState({
items: getItems(),
});
}
render() {
return <>
<List items{this.state.items}></List>
<button onClick={this.refresh}/>
</>;
}
}
規則:不要用字串拼接 url
說明:
字串拼接 url 需要處理 encode 或者 decode 的情況,還有對於 ?和 # 的判斷不對的話,很容易造成漏洞或者 Bug。
目前瀏覽器和 Node 都已經提供了標準的 URL 解析方法。
https://developer.mozilla.org...
BadCode
// 這段程式碼既沒有對 key、value 做 encode
// 也沒有考慮 url 中 # 出現的情況
const url = location.href;
if (url.indexOf('?') >= 0) {
return url + key + '=' + value;
} else {
return url + '?' + key + '=' + value;
}
GoodCode
// 使用標準的 URL 解析,風險會降低很多
const url = new URL(urlStr);
url.searchParams.set(key, value);
return url.toString();
邏輯
規則:判真不判假
說明:
我們應該期望 if 條件內是個「真」值,而不是一個「假」值。
第二種情況會導致程式碼不易理解。
解決辦法參考 布林邏輯
BadCode
// if 條件內期望的是一個「假」值
if (!(status !== Closed) { ... }
if (!(status !== Closed || type !== Array)) { ...}
GoodCode
if (status === Closed) { ... }
if (status === Closed && type === Array) { ... }
規則:if 條件中,不易出現超過 3 個邏輯操作符。
例外:if 條件裡可以被 「且」(&&)邏輯拆分成多個子條件
說明:複雜的條件判斷會讓程式碼不易理解,邏輯上有漏洞的話容易引起 Bug。
解決辦法:宣告中間變數
BadCode
if (srcElem != dropElem && (srcElem.nextSibling || srcElem.nextElementSibling) != dropElem) {...}
if (selectedItem || (selectedEmployee && selectedEmployee.empId && selectedEmployee) || employee) { ... }
GoodCode
const nextSibling = srcElem.nextSibling || srcElem.nextElementSibling
if (srcElem != dropElem && nextSibling != dropElem ) {
...
}
// 複雜的邏輯判斷可以通過 && 做拆分
if (
!Array.isArray(cur)
&& cur != null
&& typeof src[key] === 'object'
&& typeof cur === 'object'
) { ... }
規則:不要用巢狀的三元表示式
說明:
人們閱讀巢狀三元表示式時,容易混淆語法的優先順序,進而導致理解錯程式碼的含義。
對於這種情況,建議改成 if else。
如果是在 react render 裡,則建議獨立成函式。
BadCode
function render(props) {
const value = props.value;
return <>
{value < 10 ? value > 0 ? value : 200 - value : 100 - value}
</>;
}
GoodCode
function getValue(value) {
if (value < 10) {
if (value > 0) return value;
return 200 - value;
}
return 100 - value;
}
function render(props) {
const value = props.value;
return <>
{getValue(value)}
</>;
}
規則:if 條件邏輯巢狀不要超過三層
說明:過深的巢狀會導致理解困難。
解決辦法:合併判斷條件,或者獨立成函式。
BadCode
if (status = Opened) {
if (type = 'array') {
if (code = Success) {
doSomething();
}
}
}
GoodCode
if (status = Opened && type = 'array' &&code = Success) {
doSomething();
}