语言规范
语言规范
JavaScript 是一种客户端脚本语言,这里列出了编写 JavaScript 时需要遵守的规则。
类型
原始类型: 存取原始类型直接作用于值本身
- 布尔类型
- Null 类型
- Undefined 类型
- 数字类型
- BigInt 类型
- 字符串类型
- 符号类型 Symbol
const foo = 1let bar = foobar = 9console.log(foo, bar) // 1, 9
复杂类型: 访问复杂类型作用于值的引用
- object
- array
- function
const foo = [1, 2, 3]const bar = foobar[0] = 9console.log(foo[0], bar[0]) // 9, 9
引用
- 请记得
const和let都是块级作用域,var是函数级作用域
// const and let only exist in the blocks they are defined in.{let a = 1const b = 1}console.log(a) // ReferenceErrorconsole.log(b) // ReferenceError
- 对所有引用都使用
const,不要使用var,eslint: prefer-const, no-const-assign
原因:这样做可以确保你无法重新分配引用,以避免出现错误和难以理解的代码
// badvar a = 1var b = 2// goodconst a = 1const b = 2
- 如果引用是可变动的,使用
let代替var,eslint: no-var
原因:
let是块级作用域的,而不像var属于函数级作用域
// badvar count = 1if (count < 10) {count += 1}// goodlet count = 1if (count < 10) {count += 1}
对象
请使用字面量值创建对象,eslint: no-new-object
// badconst a = new Object{}// goodconst a = {}
别使用保留字作为对象的键值,这样在 IE8 下不会运行
// badconst a = {default: {}, // default 是保留字common: {}}// goodconst a = {defaults: {},common: {}}
当使用动态属性名创建对象时,请使用对象计算属性名来进行创建
原因:因为这样做就可以让你在一个地方定义所有的对象属性
function getKey(k) {return `a key named ${k}`}// badconst obj = {id: 5,name: 'San Francisco'};obj[getKey('enabled')] = true// goodconst obj = {id: 5,name: 'San Francisco',[getKey('enabled')]: true};
请使用对象方法的简写方式,eslint: object-shorthand
// badconst item = {value: 1,addValue: function (val) {return item.value + val}}// goodconst item = {value: 1,addValue (val) {return item.value + val}}
请使用对象属性值的简写方式,eslint: object-shorthand
原因:这样更简短且描述更清楚
const job = 'FrontEnd'// badconst item = {job: job}// goodconst item = {job}
将简写的对象属性分组后统一放到对象声明的开头
原因:这样更容易区分哪些属性用了简写的方式
const job = 'FrontEnd'const department = 'JDC'// badconst item = {sex: 'male',job,age: 25,department}// goodconst item = {job,department,sex: 'male',age: 25}
- 只对非法标识符的属性使用引号,eslint: quote-props
原因:因为通常来说我们认为这样主观上会更容易阅读,这样会带来代码高亮上的提升,同时也更容易被主流 JS 引擎优化
// badconst bad = {'foo': 3,'bar': 4,'data-blah': 5}// goodconst good = {foo: 3,bar: 4,'data-blah': 5}
- 不要直接使用
Object.prototype的方法, 例如hasOwnProperty,propertyIsEnumerable和isPrototypeOf方法,eslint: no-prototype-builtins原因:这些方法可能会被对象自身的同名属性覆盖 - 比如
{ hasOwnProperty: false }或者对象可能是一个null对象(Object.create(null))
// badconsole.log(object.hasOwnProperty(key))// goodconsole.log(Object.prototype.hasOwnProperty.call(object, key))// bestconst has = Object.prototype.hasOwnProperty // cache the lookup once, in module scope.console.log(has.call(object, key))/* or */import has from 'has' // https://www.npmjs.com/package/hasconsole.log(has(object, key))
- 优先使用对象展开运算符
...来做对象浅拷贝而不是使用Object.assign,使用对象剩余操作符来获得一个包含确定的剩余属性的新对象
// very badconst original = { a: 1, b: 2 }const copy = Object.assign(original, { c: 3 }) // this mutates `original` ಠ_ಠdelete copy.a // so does this// badconst original = { a: 1, b: 2 }const copy = Object.assign({}, original, { c: 3 }) // copy => { a: 1, b: 2, c: 3 }// goodconst 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 }
数组
请使用字面量值创建数组,eslint: no-array-constructor
// badconst items = new Array()// goodconst items = []
向数组中添加元素时,请使用
push方法const items = []// baditems[items.length] = 'test'// gooditems.push('test')
使用展开运算符
...复制数组// badconst items = []const itemsCopy = []const len = items.lengthlet i// badfor (i = 0; i < len; i++) {itemsCopy[i] = items[i]}// gooditemsCopy = [...items]
把一个可迭代的对象转换为数组时,使用展开运算符
...而不是Array.from
const foo = document.querySelectorAll('.foo')// goodconst nodes = Array.from(foo)// bestconst nodes = [...foo]
- 使用
Array.from来将一个类数组对象转换为数组
const arrLike = { 0: 'foo', 1: 'bar', 2: 'baz', length: 3 }// badconst arr = Array.prototype.slice.call(arrLike)// goodconst arr = Array.from(arrLike)
- 遍历迭代器进行映射时使用
Array.from代替扩展运算符..., 因为这可以避免创建中间数组
// badconst baz = [...foo].map(bar)// goodconst baz = Array.from(foo, bar)
使用数组的
map等方法时,请使用return声明,如果是单一声明语句的情况,可省略return// good[1, 2, 3].map(x => {const y = x + 1return x * y})// good[1, 2, 3].map(x => x + 1)// badconst flat = {}[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {const flatten = memo.concat(item)flat[index] = flatten})// goodconst flat = {}[[0, 1], [2, 3], [4, 5]].reduce((memo, item, index) => {const flatten = memo.concat(item)flat[index] = flattenreturn flatten})// badinbox.filter((msg) => {const { subject, author } = msgif (subject === 'Mockingbird') {return author === 'Harper Lee'} else {return false}})// goodinbox.filter((msg) => {const { subject, author } = msgif (subject === 'Mockingbird') {return author === 'Harper Lee'}return false})
如果一个数组有多行则要在数组的开括号后和闭括号前使用新行
// badconst arr = [[0, 1], [2, 3], [4, 5]]const objectInArray = [{id: 1}, {id: 2}]const numberInArray = [1, 2]// goodconst arr = [[0, 1], [2, 3], [4, 5]]const objectInArray = [{id: 1},{id: 2}]const numberInArray = [1,2]
解构赋值
- 当需要使用对象的多个属性时,请使用解构赋值,eslint: prefer-destructuring
愿意:解构可以避免创建属性的临时引用
// badfunction getFullName (user) {const firstName = user.firstNameconst lastName = user.lastNamereturn `${firstName} ${lastName}`}// goodfunction getFullName (user) {const { firstName, lastName } = userreturn `${firstName} ${lastName}`}// betterfunction getFullName ({ firstName, lastName }) {return `${firstName} ${lastName}`}
当需要使用数组的多个值时,请同样使用解构赋值,eslint: prefer-destructuring
const arr = [1, 2, 3, 4]// badconst first = arr[0]const second = arr[1]// goodconst [first, second] = arr
函数需要回传多个值时,请使用对象的解构,而不是数组的解构
原因:可以非破坏性地随时增加或者改变属性顺序
// badfunction doSomething () {return [top, right, bottom, left]}// 如果是数组解构,那么在调用时就需要考虑数据的顺序const [top, xx, xxx, left] = doSomething()// goodfunction doSomething () {return { top, right, bottom, left }}// 此时不需要考虑数据的顺序const { top, left } = doSomething()
字符串
字符串统一使用单引号的形式
'',eslint: quotes// badconst department = "JDC"// goodconst department = 'JDC'
字符串太长的时候,请不要使用字符串连接符换行
\,而是使用+const str = '凹凸实验室 凹凸实验室 凹凸实验室' +'凹凸实验室 凹凸实验室 凹凸实验室' +'凹凸实验室 凹凸实验室'
程序化生成字符串时,请使用模板字符串,eslint: prefer-template template-curly-spacing
const test = 'test'// badconst str = ['a', 'b', test].join()// badconst str = 'a' + 'b' + test// goodconst str = `ab${test}`
不要对字符串使用eval(),会导致太多漏洞, eslint: no-eval
不要在字符串中使用不必要的转义字符, eslint: no-useless-escape
// badconst foo = '\'this\' \i\s \"quoted\"'// goodconst foo = '\'this\' is "quoted"'const foo = `my name is '${name}'`
函数
- 不要使用Function构造函数创建函数, eslint: no-new-func
原因:此方式创建函数和对字符串使用
eval()一样会产生漏洞
// badconst add = new Function('a', 'b', 'return a + b')// still badconst subtract = Function('a', 'b', 'return a - b')
- 在函数签名中使用空格,eslint: space-before-function-paren space-before-blocks
const f = function(){}const g = function (){}const h = function() {}// goodconst x = function b () {}const y = function a () {}
- 使用具名函数表达式而非函数声明,eslint: func-style
原因:这样做会导致函数声明被提升,这意味着很容易在文件中定义此函数之前引用它,不利于可读性和可维护性。如果你发现函数定义既庞大又复杂以至于不能理解文件的其他部分,或许你应该将它拆分成模块!别忘记要显式命名表达式,而不用管名字是否是从包含的变量(通常出现在现代浏览器中或者使用 Babel 编译器的时候)中推断的。这样会消除错误调用堆栈中的任何假设。 (讨论)
// badfunction foo () {// ...}// badconst foo = function () {// ...}// good// lexical name distinguished from the variable-referenced invocation(s)const short = function longUniqueMoreDescriptiveLexicalFoo () {// ...}
- 用圆括号包裹自执行匿名函数,eslint:wrap-iife
原因:一个立即执行匿名函数表达式是一个单一的单元,将其及其调用括号包装在括号中,能够清楚地表达这一点。注意,在到处都是模块的世界中几乎不需要 IIFE。
// immediately-invoked function expression (IIFE)(function () {console.log('Welcome to the Internet. Please follow me.')}())
不要在非函数代码块(
if,while等)中声明函数,eslint:no-loop-func// badif (isUse) {function test () {// do something}}// goodlet testif (isUse) {test = () => {// do something}}
不要将参数命名为
arguments,会导致该参数的优先级高于每个函数作用域内原先存在的arguments对象
// badfunction foo (name, options, arguments) {// ...}// goodfunction foo (name, options, args) {// ...}
不要使用
arguments,使用 剩余运算符...arguments只是一个类数组,而...是一个真正的数组// badfunction test () {const args = Array.prototype.slice.call(arguments)return args.join('')}// goodfunction test (...args) {return args.join('')}
使用参数默认值语法而不是修改函数参数
// really badfunction handleThings (opts) {// No! We shouldn't mutate function arguments.// Double bad: if opts is falsy it'll be set to an object which may// be what you want but it can introduce subtle bugs.opts = opts || {}// ...}// still badfunction handleThings (opts) {if (opts === void 0) {opts = {}}// ...}// goodfunction handleThings (opts = { }) {// ...}
- 避免参数默认值的副作用
let b = 1// badfunction count (a = b++) {console.log(a)}count() // 1count() // 2count(3) // 3count() // 3
- 将参数默认值放在最后
// badfunction handleThings (opts = {}, name) {// ...}// goodfunction handleThings (name, opts = {}) {// ...}
- 不要更改参数,eslint: no-param-reassign
原因:操作作为参数传入的对象可能在原始调用中造成意想不到的变量副作用
// badfunction f1 (obj) {obj.key = 1}// goodfunction f2 (obj) {const key = Object.prototype.hasOwnProperty.call(obj, 'key') ? obj.key : 1}
- 不要给参数重新赋值,eslint: no-param-reassign
原因:参数重新赋值可能会导致无法预期的行为,尤其是当操作
arguments对象时,也可能导致优化问题,尤其是在 V8 引擎中
// badfunction f1 (a) {a = 1}function f2 (a) {if (!a) { a = 1 }}// goodfunction f3 (a) {const b = a || 1}function f4 (a = 1) {}
- 调用可变参数函数时建议使用展开运算符
...., eslint: prefer-spread
原因:显然你无需使用上下文,很难结合
new和apply
// badconst x = [1, 2, 3, 4, 5]console.log.apply(console, x)// goodconst x = [1, 2, 3, 4, 5]console.log(...x)// badnew (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]))// goodnew Date(...[2016, 8, 5])
箭头函数
- 当你必须使用函数表达式(传递匿名函数)时,使用箭头函数标记. eslint: prefer-arrow-callback, arrow-spacing
原因:它将创建在
this上下文中执行的函数版本,通常是您想要的,并且语法更简洁如果您有一个相当复杂的函数,则可以将该逻辑移到其自己的命名函数表达式中
// bad[1, 2, 3].map(function (x) {const y = x + 1return x * y})// good[1, 2, 3].map((x) => {const y = x + 1return x * y})
- 如果函数体只包含一条没有副作用的返回表达式的语句,可以省略花括号并使用隐式的
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 + 1return `A string containing the ${nextNumber}.`})// good[1, 2, 3].map((number, index) => ({index: number}))// No implicit return with side effectsfunction foo(callback) {const val = callback()if (val === true) {// Do something if callback returns true}}let bool = false// badfoo(() => bool = true)// goodfoo(() => {bool = true})
- 一旦表达式跨多行,使用圆括号包裹以便更好阅读
// bad['get', 'post', 'put'].map(httpMethod => Object.prototype.hasOwnProperty.call(httpMagicObjectWithAVeryLongName,httpMethod))// good['get', 'post', 'put'].map(httpMethod => (Object.prototype.hasOwnProperty.call(httpMagicObjectWithAVeryLongName,httpMethod)))
- 函数如果只接收一个参数并且没使用用花括号,则省略圆括号,否则为了清晰明确则使用圆括号包裹参数,注意:总是使用圆括号也是可以接受的,eslint 中的 “always” 选项,eslint: arrow-parens
// bad[1, 2, 3].map((x) => x * x)// good[1, 2, 3].map(x => x * x)// good[1, 2, 3].map(number => (`A long string with the ${number}. It’s so long that we’ve broken it ` +'over multiple lines!'))// bad[1, 2, 3].map(x => {const y = x + 1return x * y})// good[1, 2, 3].map((x) => {const y = x + 1return x * y})
类&构造函数
使用
class,避免直接操作prototype// badfunction Queue (contents = []) {this._queue = [..contents]}Queue.prototype.pop = function () {const value = this._queue[0]this._queue.splice(0, 1)return value}// goodclass Queue {constructor (contents = []) {this._queue = [...contents]}pop () {const value = this._queue[0]this._queue.splice(0, 1)return value}}
使用
extends来实现继承
原因:这是一个不会破坏
instanceof的内建实现原型式继承的方式
// badconst inherits = require('inherits')function PeekableQueue(contents) {Queue.apply(this, contents)}inherits(PeekableQueue, Queue)PeekableQueue.prototype.peek = function () {return this.queue[0]}// goodclass PeekableQueue extends Queue {peek () {return this.queue[0]}}
- 如果未声明构造函数,则类会有一个默认的构造函数,没必要用空的构造函数或者将其委托给父类,eslint: no-useless-constructor
// badclass Jedi {constructor () {}getName() {return this.name}}// badclass Rey extends Jedi {constructor (...args) {super(...args)}}// goodclass Rey extends Jedi {constructor (...args) {super(...args)this.name = 'Rey'}}
- 避免类成员重复,eslint: no-dupe-class-members
原因:重复的类成员声明会默认使用最后声明的,通常会导致 bug
// badclass Foo {bar () { return 1 }bar () { return 2 }}// goodclass Foo {bar () { return 1 }}// goodclass Foo {bar () { return 2 }}
模块
- 使用标准的 ES6 模块语法
import和export
原因:模块是未来,让我们现在开始使用未来的特性
// badconst util = require('./util')module.exports = util// goodimport Util from './util'export default Util// betterimport { Util } from './util'export default Util
不要使用
import的通配符*,这样可以确保你只有一个默认的 export// badimport * as Util from './util'// goodimport Util from './util'
同个文件每个模块只允许
import一次,有多个import请书写在一起,eslint: no-duplicate-imports
原因:这样可以让代码更易于维护
// badimport foo from 'foo'// … some other imports … //import { named1, named2 } from 'foo'// goodimport foo, { named1, named2 } from 'foo'// goodimport foo, {named1,named2} from 'foo'
- 将所有
import语句放在文件最前方,eslint: import/imports-first
// badimport foo from 'foo'foo.init()import bar from 'bar'// goodimport foo from 'foo'import bar from 'bar'foo.init()
- 多行导入应该像多行数组和对象文字一样缩进
// badimport { longNameA, longNameB, longNameC, longNameD, longNameE } from 'path'// goodimport {longNameA,longNameB,longNameC,longNameD,longNameE} from 'path'
- 在模块
import声明中禁止使用Webpack的loader语法,eslint: import/no-webpack-loader-syntax
// badimport fooSass from 'css!sass!foo.scss'import barCss from 'style!css!bar.css'// goodimport fooSass from 'foo.scss'import barCss from 'bar.css'
迭代器
- 不要使用
iterators,建议使用 JS 更高优先级的函数代替 for-in 或 for-of 循环,除非迫不得已,eslint: no-iterator no-restricted-syntax
const numbers = [1, 2, 3, 4, 5]// badlet sum = 0for (let num of numbers) {sum += num}// goodlet sum = 0numbers.forEach(num => sum += num)// betterconst sum = numbers.reduce((total, num) => total + num, 0)
生成器
- 现阶段请不要使用生成器
generator
原因:因为不能很好地翻译成 ES5 代码
对象属性
使用
.来访问对象属性const joke = {name: 'haha',age: 28}// badconst name = joke['name']// goodconst name = joke.name
- 当访问的属性是变量时使用
[]
const luke = {jedi: true,age: 28,}function getProp (prop) {return luke[prop]}const isJedi = getProp('jedi')
变量声明
声明变量时,请使用
const、let关键字,如果没有写关键字,变量就会暴露在全局上下文中,这样很可能会和现有变量冲突,另外,也很难明确该变量的作用域是什么。这里推荐使用const来声明变量,我们需要避免全局命名空间的污染。eslint: no-undef prefer-const// baddemo = new Demo()// goodconst demo = new Demo()
将所有的
const和let分组// badlet aconst blet cconst dlet e// goodconst bconst dlet alet clet e
变量不要进行链式赋值
原因:变量链式赋值会创建隐藏的全局变量
// bad(function example() {// JavaScript interprets this as// let a = ( b = ( c = 1 ) );// The let keyword only applies to variable a; variables b and c become// global variables.let a = b = c = 1}())console.log(a) // throws ReferenceErrorconsole.log(b) // 1console.log(c) // 1// good(function example() {let a = 1let b = alet c = a}())console.log(a) // throws ReferenceErrorconsole.log(b) // throws ReferenceErrorconsole.log(c) // throws ReferenceError// the same applies for `const`
- 不允许出现未被使用的变量,eslint: no-unused-vars
原因:声明但未被使用的变量通常是不完全重构犯下的错误.这种变量在代码里浪费空间并会给读者造成困扰
// badvar some_unused_var = 42// Write-only variables are not considered as used.var y = 10y = 5// A read for a modification of itself is not considered as used.var z = 0z = z + 1// Unused function arguments.function getX (x, y) {return x}// goodfunction getXPlusY (x, y) {return x + y}const x = 1const y = a + 2alert(getXPlusY(x, y))// 'type' is ignored even if unused because it has a rest property sibling.// This is a form of extracting an object that omits the specified keys.const { type, ...coords } = data// 'coords' is now the 'data' object without its 'type' property.
Hoisting
var存在变量提升的情况,即var声明会被提升至该作用域的顶部,但是他们的赋值并不会。而const和let并不存在这种情况,他们被赋予了 Temporal Dead Zones, TDZ, 了解 typeof 不再安全很重要function example () {console.log(notDefined) // => throws a ReferenceError}function example () {console.log(declareButNotAssigned) // => undefinedvar declaredButNotAssigned = true}function example () {let declaredButNotAssignedconsole.log(declaredButNotAssigned) // => undefineddeclaredButNotAssigned = true}function example () {console.log(declaredButNotAssigned) // => throws a ReferenceErrorconsole.log(typeof declaredButNotAssigned) // => throws a ReferenceErrorconst declaredButNotAssigned = true}
匿名函数的变量名会提升,但函数内容不会
function example () {console.log(anonymous) // => undefinedanonymous()var anonymous = function () {console.log('test')}}
命名的函数表达式的变量名会被提升,但函数名和函数函数内容并不会
function example() {console.log(named) // => undefinednamed() // => TypeError named is not a functionsuperPower() // => ReferenceError superPower is not definedvar named = function superPower () {console.log('Flying')}}function example() {console.log(named) // => undefinednamed() // => TypeError named is not a functionvar named = function named () {console.log('named')}}
比较运算符&相等
使用
===和!==而非==和!=,eslint: eqeqeq条件声明例如
if会用ToBoolean这个抽象方法将表达式转成布尔值并遵循如下规则Objects等于trueUndefined等于falseNull等于falseBooleans等于布尔值Numbers在+0,-0, 或者NaN的情况下等于false, 其他情况是trueStrings为''时等于false, 否则是trueif ([0] && []) {// true// 数组(即使是空数组)也是对象,对象等于true}
分号
我们遵循
Standard的规范,不使用分号。关于应不应该使用分号的讨论有很多,本规范认为非必要的时候,应该不使用分号,好的
JS程序员应该清楚场景下是一定要加分号的,相信你也是名好的开发者。// badconst test = 'good';(function () {const str = 'hahaha';})()// goodconst test = 'good';(() => {const str = 'hahaha'})();
标准特性
为了代码的可移植性和兼容性,我们应该最大化的使用标准方法,例如优先使用 string.charAt(3) 而不是 string[3]
eval()
由于 eval 方法比较 evil,所以我们约定禁止使用该方法
with() {}
由于 with 方法会产生神奇的作用域,所以我们也是禁止使用该方法的
修改内置对象的原型
不要修改内置对象,如 Object 和 Array
