前端开发规范
1、代码规范
类型
- 1.1 基本类型
- 基本类型赋值时,应该直接使用类型的值
string
number
boolean
null
undefined
symbol
- 基本类型赋值时,应该直接使用类型的值
const foo = 1; let bar = foo; bar = 9; console.log(foo, bar); // => 1,9 |
-
- Symbols 不能被完全 polyfill, 所以不应该在目标浏览器/环境不支持它们的情况下使用它们。
-
- 复杂类型
object
array
function
- 复杂类型
const foo = [1, 2]; const bar = foo; bar[0] = 9; console.log(foo[0], bar[0]); // => 9, 9 // const 只能阻止引用类型地址的重新赋值 // 并不能保证引用类型的属性等不变 |
引用 Reference
- 2.1 所有的赋值都用
const
,避免使用var
. eslint:prefer-const
,no-const-assign
- 尽量确保你的代码中的状态是可控范围内的,重复引用会出现难以理解的 bug 和代码。
// bad var a = 1; var b = 2; // good const a = 1; const b = 2; |
- 2.2 如果你一定要对参数重新赋值,那就用
let
,而不是var
. eslint:no-var
let
是块级作用域,var
是函数级作用域,同样是为了减少代码的不可控,减少 “意外”
// bad var count = 1; if (true) { count += 1; } // good, use the let. let count = 1; if (true) { count += 1; } |
- 2.3
let
、const
都是块级作用域
// const 和 let 都只存在于它定义的那个块级作用域 { let a = 1; const b = 1; } console.log(a); // ReferenceError,引用错误 console.log(b); // ReferenceError,引用错误 |
对象 Objects
- 3.1 使用字面值创建对象. eslint:
no-new-object
// bad const item = new Object(); // good const item = {}; |
- 3.2 属性值缩写. eslint:
object-shorthand
const lukeSkywalker = 'Luke Skywalker'; // bad const obj = { lukeSkywalker: lukeSkywalker, }; // good const obj = { lukeSkywalker }; |
- 3.3 将属性的缩写放在对象声明的开头。
const anakinSkywalker = 'Anakin Skywalker'; const lukeSkywalker = 'Luke Skywalker'; // bad const obj = { episodeOne: 1, twoJediWalkIntoACantina: 2, lukeSkywalker, episodeThree: 3, mayTheFourth: 4, anakinSkywalker, }; // good const obj = { lukeSkywalker, anakinSkywalker, episodeOne: 1, twoJediWalkIntoACantina: 2, episodeThree: 3, mayTheFourth: 4, }; |
- 3.4 对象浅拷贝时,更推荐使用扩展运算符
...
,而不是Object.assign
。解构赋值获取对象指定的几个属性时,推荐用 rest 运算符,也是...
// very bad const original = { a: 1, b: 2 }; const copy = Object.assign(original, { c: 3 }); delete copy.a; // so does this 改变了 original // bad const original = { a: 1, b: 2 }; const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 } // good const original = { a: 1, b: 2 }; const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 } const { a, ...noA } = copy; // noA => { b: 2, c: 3 } |
数组 Arrays
- 4.1 用字面量赋值。 eslint:
no-array-constructor
// bad const items = new Array(); // good const items = []; |
- 4.2 用Array#push 向数组中添加一个值而不是直接用下标。
const someStack = []; // bad someStack[someStack.length] = 'abracadabra'; // good someStack.push('abracadabra'); |
- 4.3 用扩展运算符做数组浅拷贝,类似上面的对象浅拷贝
// bad const len = items.length; const itemsCopy = []; let i; for (i = 0; i < len; i += 1) { itemsCopy[i] = items[i]; } // good const itemsCopy = [...items]; |
- 4.4 推荐用
...
运算符而不是Array.from
来将一个类数组转换成数组。
const foo = document.querySelectorAll('.foo'); // good const nodes = Array.from(foo); // best const nodes = [...foo]; |
- 4.5 用
Array.from
去将一个类数组对象转成一个数组。
const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }; // bad const arr = Array.prototype.slice.call(arrLike); // good const arr = Array.from(arrLike); |
解构 Destructuring
- 5.1 用对象的解构赋值来获取和使用对象某个或多个属性值。 eslint:
prefer-destructuring
// bad function getFullName(user) { const firstName = user.firstName; const lastName = user.lastName; return `${firstName} ${lastName}`; } // good function getFullName(user) { const { firstName, lastName } = user; return `${firstName} ${lastName}`; } // best function getFullName({ firstName, lastName }) { return `${firstName} ${lastName}`; } |
- 5.2 数组解构
const arr = [1, 2, 3, 4]; // bad const first = arr[0]; const second = arr[1]; // good const [first, second] = arr; |
字符串 Strings
- 6.1 string 统一用单引号
''
。 eslint:quotes
// bad const name = "Capt. Janeway"; // bad - 模板应该包含插入文字或换行 const name = `Capt. Janeway`; // good const name = 'Capt. Janeway'; |
- 6.2 用字符串模板而不是
+
来拼接字符串。 eslint:prefer-template
template-curly-spacing
// 模板字符串更具可读性、语法简洁、字符串插入参数。 // bad function sayHi(name) { return 'How are you, ' + name + '?'; } // bad function sayHi(name) { return ['How are you, ', name, '?'].join(); } // good function sayHi(name) { return `How are you, ${name}?`; } |
- 6.3 永远不要在字符串中用
eval()
,漏洞太多。 eslint:no-eval
- 6.4 不要使用不必要的转义字符。eslint:
no-useless-escape
反斜线可读性差,只在必要时使用
// 反斜线可读性差,只在必要时使用 // bad const foo = '\'this\' \i\s \"quoted\"'; // good const foo = '\'this\' is "quoted"'; //best const foo = `my name is '${name}'`; |
函数 Functions
- 7.1 把立即执行函数包裹在圆括号里。 eslint:
wrap-iife
// immediately-invoked function expression (IIFE) (function () { console.log('Welcome to the Internet. Please follow me.'); }()); |
-
7.2 注意: 在ECMA-262中 [块
block
] 的定义是: 一系列的语句; 但是函数声明不是一个语句。 函数表达式是一个语句。
// bad if (currentUser) { function test() { console.log('Nope.'); } } // good let test; if (currentUser) { test = () => { console.log('Yup.'); }; } |
- 7.3 永远不要用
arguments
命名参数。它的优先级高于每个函数作用域自带的arguments
对象, 所以会导致函数自带的arguments
值被覆盖。
// bad function foo(name, options, arguments) { // ... } // good function foo(name, options, args) { // ... } |
- 7.4 优先使用rest语法
...
,而不是arguments
。 eslint:prefer-rest-params
// bad function concatenateAll() { const args = Array.prototype.slice.call(arguments); return args.join(''); } // good function concatenateAll(...args) { return args.join(''); } |
- 7.5 使用默认参数语法,而不是在函数里对参数重新赋值。
// really bad function handleThings(opts) { // 虽然你想这么写, 但是这个会带来一些细微的bug // 如果 opts 的值为 false, 它会被赋值为 {} opts = opts || {}; // ... } // still bad function handleThings(opts) { if (opts === void 0) { opts = {}; } // ... } // good function handleThings(opts = {}) { // ... } |
- 7.6 使用默认参数时,需要避免副作用
var b = 1; // bad function count(a = b++) { console.log(a); } count(); // 1 count(); // 2 count(3); // 3 count(); // 3 // 很容易让人懵逼 |
- 7.7 把默认参数赋值放在最后
// bad function handleThings(opts = {}, name) { // ... } // good function handleThings(name, opts = {}) { // ... } |
- 7.8 函数签名部分要有空格。eslint:
space-before-function-paren
space-before-blocks
// bad const f = function(){}; const g = function (){}; const h = function() {}; // good const x = function () {}; const y = function a() {}; |
- 7.9 永远不要改参数. eslint:
no-param-reassign
// 特别注意引用类型的操作,保证数据的不可变性 // bad function f1(obj) { obj.key = 1; }; // good function f2(obj) { const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1; }; |
- 7.10 不要对参数重新赋值。 eslint:
no-param-reassign
// bad function f1(a) { a = 1; // ... } function f2(a) { if (!a) { a = 1; } // ... } // good function f3(a) { const b = a || 1; // ... } function f4(a = 1) { // ... } |
箭头函数 Arrow Functions
- 8.1 如果要用匿名函数做回调,最好使用箭头函数 eslint:
prefer-arrow-callback
,arrow-spacing
// 它创建了一个在上下文中执行的函数,这通常是您想要的,并且是一种更简洁的语法。 // bad [1, 2, 3].map(function (x) { const y = x + 1; return x * y; }); // good [1, 2, 3].map((x) => { const y = x + 1; return x * y; }); |
- 8.2 如果函数体由一个没有副作用的表达式的单个语句组成,去掉大括号和 return。否则,保留大括号且使用
return
语句。 eslint:arrow-parens
,arrow-body-style
// bad [1, 2, 3].map(number => { const nextNumber = number + 1; `A string containing the ${nextNumber}.`; }); // good [1, 2, 3].map(number => `A string containing the ${number}.`); // good [1, 2, 3].map((number) => { const nextNumber = number + 1; return `A string containing the ${nextNumber}.`; }); // good [1, 2, 3].map((number, index) => ({ [index]: number })); // 表达式有副作用就不要用隐式返回 function foo(callback) { const val = callback(); if (val === true) { // Do something if callback returns true } } let bool = false; // bad foo(() => bool = true); // good foo(() => { bool = true; }); |
- 8.3 如果表达式有多行,首尾放在圆括号里更可读。
// bad ['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call( httpMagicObjectWithAVeryLongName, httpMethod ) ); // good ['get', 'post', 'put'].map(httpMethod => ( Object.prototype.hasOwnProperty.call( httpMagicObjectWithAVeryLongName, httpMethod ) )); |
- 8.4 避免箭头函数语法
=>
和比较操作符<=, >=
混淆. eslint:no-confusing-arrow
// bad const itemHeight = item => item.height > 256 ? item.largeSize : item.smallSize; // bad const itemHeight = (item) => item.height > 256 ? item.largeSize : item.smallSize; // good const itemHeight = item => (item.height > 256 ? item.largeSize : item.smallSize); // good const itemHeight = (item) => { const { height, largeSize, smallSize } = item; return height > 256 ? largeSize : smallSize; }; |
类 Classes & 构造函数 Constructors
- 9.1 始终用
class
,避免直接操作prototype
// bad function Queue(contents = []) { this.queue = [...contents]; } Queue.prototype.pop = function () { const value = this.queue[0]; this.queue.splice(0, 1); return value; }; // good class Queue { constructor(contents = []) { this.queue = [...contents]; } pop() { const value = this.queue[0]; this.queue.splice(0, 1); return value; } } |
- 9.2 如果没有特殊说明,类有默认的构造方法。不用特意写一个空的构造函数或只是代表父类的构造函数。 eslint:
no-useless-constructor
// bad class Jedi { constructor() {} getName() { return this.name; } } // bad class Rey extends Jedi { // 这种构造函数是不需要写的 constructor(...args) { super(...args); } } // good class Rey extends Jedi { constructor(...args) { super(...args); this.name = 'Rey'; } } |
模块 Modules
- 10.1
import
放在其他所有语句之前。 eslint:import/first
// bad import foo from 'foo'; foo.init(); import bar from 'bar'; // good import foo from 'foo'; import bar from 'bar'; foo.init(); |
- 10.2 不要导出可变的绑定 eslint:
import/no-mutable-exports
// 尽量减少状态,保证数据的不可变性。虽然在某些场景下可能需要这种技术,但总的来说应该导出常量。 // bad let foo = 3; export { foo } // good const foo = 3; export { foo } |
- 10.3 在只有一个导出的模块里,用
export default
更好。 eslint:import/prefer-default-export
// bad export function foo() {} // good export default function foo() {} |
属性 Properties
- 11.1 访问属性时使用点符号. eslint:
dot-notation
const luke = { jedi: true, age: 28, }; // bad const isJedi = luke['jedi']; // good const isJedi = luke.jedi; |
- 11.2 获取的属性是变量时用方括号
[]
const luke = { jedi: true, age: 28, }; function getProp(prop) { return luke[prop]; } const isJedi = getProp('jedi'); |
- 11.3 做幂运算时用幂操作符
**
。 eslint:no-restricted-properties
.
// bad const binary = Math.pow(2, 10); // good const binary = 2 ** 10; |
变量 Variables
- 12.1 始终用
const
或let
声明变量。如果你不想遇到一对变量提升、全局变量的 bug 的话。 eslint:no-undef
prefer-const
// bad superPower = new SuperPower(); // good const superPower = new SuperPower(); |
- 12.2 每个变量单独用一个
const
或let
。 eslint:one-var
// bad const items = getItems(), goSportsTeam = true, dragonball = 'z'; // bad // (compare to above, and try to spot the mistake) const items = getItems(), goSportsTeam = true; dragonball = 'z'; // good const items = getItems(); const goSportsTeam = true; const dragonball = 'z'; |
- 12.3
const
放一起,let
放一起
// bad let i, len, dragonball, items = getItems(), goSportsTeam = true; // bad let i; const items = getItems(); let dragonball; const goSportsTeam = true; let len; // good const goSportsTeam = true; const items = getItems(); let dragonball; let i; let length; |
- 12.4 变量声明放在合理的位置
// bad - unnecessary function call function checkName(hasName) { const name = getName(); if (hasName === 'test') { return false; } if (name === 'test') { this.setName(''); return false; } return name; } // good function checkName(hasName) { if (hasName === 'test') { return false; } // 在需要的时候分配 const name = getName(); if (name === 'test') { this.setName(''); return false; } return name; } |
- 12.5 不允许有未使用的变量。 eslint:
no-unused-vars
// bad var some_unused_var = 42; // 定义了没有使用 var y = 10; y = 5; // 不会将用于修改自身的读取视为已使用 var z = 0; z = z + 1; // 参数定义了但未使用 function getX(x, y) { return x; } // good function getXPlusY(x, y) { return x + y; } var x = 1; var y = a + 2; alert(getXPlusY(x, y)); // 'type' 即使没有使用也可以被忽略, 因为这个有一个 rest 取值的属性。 // 这是从对象中抽取一个忽略特殊字段的对象的一种形式 var { type, ...coords } = data; // 'coords' 现在就是一个没有 'type' 属性的 'data' 对象 |
-
12.6 避免在
=
前/后换行。 如果你的语句超出max-len
, 那就用()
把这个值包起来再换行。 eslintoperator-linebreak
.// bad const foo = superLongLongLongLongLongLongLongLongFunctionName(); // bad const foo = 'superLongLongLongLongLongLongLongLongString'; // good const foo = ( superLongLongLongLongLongLongLongLongFunctionName() ); // good const foo = 'superLongLongLongLongLongLongLongLongString';
比较和相等
-
13.1
if
等条件语句使用强制ToBoolean
抽象方法来评估它们的表达式,并且始终遵循以下简单规则:
-
-
Objects => true
-
Undefined => false
-
Null => false
-
Booleans => the value of the boolean
-
Numbers
- +0, -0, or NaN => false
- 其他 => true
-
Strings
''
=> false- 其他 => true
-
- 13.2 布尔值比较可以省略,但是字符串和数字要显示比较
// bad if (isValid === true) { // ... } // good if (isValid) { // ... } // bad if (name) { // ... } // good if (name !== '') { // ... } // bad if (collection.length) { // ... } // good if (collection.length > 0) { // ... } |
- 13.3 三元表达式不应该嵌套,通常是单行表达式。eslint rules:
no-nested-ternary
.
// bad const foo = maybe1 > maybe2 ? "bar" : value1 > value2 ? "baz" : null; // better const maybeNull = value1 > value2 ? 'baz' : null; const foo = maybe1 > maybe2 ? 'bar' : maybeNull; // best const maybeNull = value1 > value2 ? 'baz' : null; const foo = maybe1 > maybe2 ? 'bar' : maybeNull; |
- 13.4 避免不需要的三元表达式 eslint rules:
no-unneeded-ternary
.
// bad const foo = a ? a : b; const bar = c ? true : false; const baz = c ? false : true; // good const foo = a || b; const bar = !!c; const baz = !c; |
块 Blocks
- 14.1 用大括号
{}
包裹多行代码块。 eslint:nonblock-statement-body-position
// bad if (test) return false; // good if (test) return false; // good if (test) { return false; } // bad function foo() { return false; } // good function bar() { return false; } |
- 14.2
else
和if
的大括号保持在一行。 eslint:brace-style
// bad if (test) { thing1(); thing2(); } else { thing3(); } // good if (test) { thing1(); thing2(); } else { thing3(); } |
注释 Comments
- 15.1 多行注释用
/** ... */
// bad // make() returns a new element // based on the passed in tag name // // @param {String} tag // @return {Element} element function make(tag) { // ... return element; } // good /** * make() returns a new element * based on the passed-in tag name */ function make(tag) { // ... return element; } |
- 15.2 单行注释使用
//
。将单行注释放在续注释的语句上方。在注释之前放置一个空行,除非它位于代码块的第一行。
// bad const active = true; // is current tab // good // is current tab const active = true; // bad function getType() { console.log('fetching type...'); // set the default type to 'no type' const type = this.type || 'no type'; return type; } // good function getType() { console.log('fetching type...'); // set the default type to 'no type' const type = this.type || 'no type'; return type; } // also good function getType() { // set the default type to 'no type' const type = this.type || 'no type'; return type; } |
命名约定
- 16.1 避免用一个字母命名,让你的命名更加语义化。 eslint:
id-length
// bad function q() { // ... } // good function query() { // ... } |
- 16.2 不要用前置或后置下划线。 eslint:
no-underscore-dangle
// bad this.__firstName__ = 'Panda'; this.firstName_ = 'Panda'; this._firstName = 'Panda'; // good this.firstName = 'Panda'; |
- 16.3 不要保存
this
的引用,使用箭头函数或硬绑定。
// bad function foo() { const self = this; return function () { console.log(self); }; } // bad function foo() { const that = this; return function () { console.log(that); }; } // good function foo() { return () => { console.log(this); }; } |
- 16.4 默认导出(
export default
)一个函数时,函数名、文件名统一。
function makeStyleGuide() { // ... } export default makeStyleGuide; |
- 16.5 简称和首字母缩写应该全部大写或全部小写。
// bad import SmsContainer from './containers/SmsContainer'; // bad const HttpRequests = [ // ... ]; // good import SMSContainer from './containers/SMSContainer'; // good const HTTPRequests = [ // ... ]; // best import TextMessageContainer from './containers/TextMessageContainer'; // best const Requests = [ // ... ]; |
- 16.6 全大写字母定义用来导出的常量
// allowed but does not supply semantic value export const apiKey = 'SOMEKEY'; // better in most cases export const API_KEY = 'SOMEKEY'; // --- // bad - unnecessarily uppercases key while adding no semantic value export const MAPPING = { KEY: 'value' }; // good export const MAPPING = { key: 'value' }; |
2、接口数据处理
-
增强前端代码的健壮性
异常处理
主要指数据类型。这种情况最常出现在读取后台数据的时候,尤其是需要取数组、对象等引用类型时,可能后台已经保证会传给你固定的数据类型,但因为种种原因或异常,可能出现本来应该是一个对象或数组的字段变成了null,这是非常之普遍的,如果前端直接取res.xxx或res.length,就会报错,导致程序阻塞。
一般有两种手法来处理这种情况:一种是判空处理,如(res || []).length,(res || {}).name等;另外一种是全部按理想情况写,但是外层用try{...}catch(){...}包裹,一有异常就抛出去,通过过滤错误来确保try后面的代码仍能正常执行。
数据检验
最常出现在表单输入时,用户可能输入各种各样奇怪的东西,比如有前后空格、负数、小数、很长很长的数,重复的数等等,一般我们可以设置一些规则来限制用户操作,比如设置最大输入长度,不能输入负数、小数等等,还有就是表单提交时对不合理的行为做出提示,阻止其进行下一步操作。
能应对怪异的用户行为
我们很难去规范用户使用系统或浏览页面的行为,尤其是行为顺序,可能用户会完全不按照正常的顺序去做一些操作,比如按照相反的顺序来,或交叉顺序操作系统,或疯狂的点击按钮。这不是普遍的情况,但最好能保证你的程序在这种情况下仍然可用, 一般应对疯狂的用户行为,可以用防抖、节流、最大次数限制来做规范,而对于不按套路操作的用户,要么限制其行为,告诉他“请先选择xxxx,再进行xxxx”,或者在程序方面做好兼容。
注意JS浮点数运算的坑
这是一个历史遗留问题,0.1 + 0.2 !== 0.3是一个大家都知道的梗,所以如果涉及到前端运算的页面,一定要注意浮点数的问题, 常见的手法有用+‘xxx’或parseInt或parseFloat来做字符串转数字,用toFixed来限定小数点的位数等。
-
更安全地访问对象
不要相信接口数据:
// 比如某个api/xxx/list的接口,按照文档的约定 { code: 0, msg: "", data: [ // ... 具体数据 ], }; // 前端代码可能就会写成 const {code, msg, data} = await fetchList() data.forEach(()=>{}) |
因为我们假设了后台返回的data是一个数组,所以直接使用了data.forEach
,如果在联调的时候遗漏了一些异常情况
-
- 预期在没有数据时data会返回
[]
空数组,但后台的实现却是不返回data
字段 - 后续接口更新,data从数组变成了一个对象,跟前端同步不及时
- 预期在没有数据时data会返回
这些时候,使用data.forEach
时就会报错。(Uncaught TypeError: data.forEach is not a function)
所以在这些直接使用后台接口返回值的地方,最好添加类型检测:
Array.isArray(data) && data.forEach(()=>{})
空值合并运算符
由于JavaScript动态特性,我们在查询对象某个属性时如x.y.z
,最好检测一下x
和y
是否存在:let z = x && x.y && x.y.z
经常这么写就显得十分麻烦,可选链操作符 安全访问对象属性就简单得多:var z = a?.y?.z;