JavaScript 代码质量优化浅谈

随着前端模块化技术的引入, 前端开发越来越趋向于后端的开发模式, 一个页面的 JavaScript 代码量在飞速增长, 特别是 WebApp 的开发。为了减少理解自己之前编写的代码的时间, 或节省同事读懂自己代码的时间 (减少被吐嘈的次数), 如何编写高质量的代码是码农们都需要关注的。

参考书籍: JavaScript Patterns (O’Reilly)。

高质量代码标准

  • 阅读性好
  • 具有一致性
  • 预见性好
  • 看起来如同一个人编写
  • 有文档

详细的编码规范可以参见 Airbnb 的 JavaScript 编码规范, 本文不详述。 Airbnb JavaScript
JavaScript 代码也可以使用代码静态检查工具 ESlint 来审查, 具体参见博客 ESLint 配置

JavaScript 编码注意事项

下面内容就介绍一些开发中 JavaScript 编写过程中需要注意的问题

尽量减少全局变量使用

每个 JavaScript 执行环境都有一个全局对象 例如: 浏览器窗口的 window 对象, 可在函数外部使用 this 关键字进行访问。JavaScript 中在函数外部创建的每一个变量都是全局变量, 可以在整个执行环境中的各个地方使用 例如:下面的定义 globalVar 变量可以在代码块中的各个部分访问, 即使在不同的 script 标签内。
而在 function foo 中定义的 localVar 变量, 就只局限于 foo 函数的作用域内可以访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
var globalVar = 'feifeiyu'
function foo() {
var localVar = 'feiyu'
console.log('global foo:', globalVar) // => feifeiyu
console.log('local foo:', localVar) // => feiyu
}
console.log('global:', globalVar) // => feifeiyu
console.log('global window1:', window.globalVar) // => feifeiyu
console.log('global window2:', window['globalVar']) // => feifeiyu
// console.log('local:', localVar) // error
</script>
<script>
console.log('global2:', globalVar)
</script>

由上例中想到, 如果一个页面中如果有两块独立的代码中, 包含有相同名称的变量, 但却有着不同的作用时, 会引起程序运行出错。在其他的程序中为了解决这个问题引入了命名空间的概念, 例如: C++ 有个专门的关键字 namespace 来声明命名空间。在 JavaScript 中没这样的关键字来控制命名空间, 因此, 为了避免变量在全局作用域中冲突, 在同一个页面中应该尽可能地减少全局作用域中的使用。如何减少全局变量数量, 大致的解决方案有两种, 一种是人为去构造 JavaScript 的命名空间, 这种模式相对复杂不常用; 另一种是采用自执行即使函数(self-execting immediate funtionsl)来实现。

自执行立即函数

采用自执行立即函数实现减少全局变量名的方式本质上还是,利用 JavaScript 的函数作用域来实现, 例如下面代码中定义的 tmpVar 只能在自执行立即函数中有效, 而在其他代码块中无效。 因此, 在页面开发时可以使用自执行立即函数来封装不同的代码, 不同代码中定义的变量都只在相应的自执行立即函数中有效, 从而减少全局变量的定义。 这个在 JavaScript 组件开发中常用。

1
2
3
4
5
6
7
8
9
10
11
<script>
(function() {
var tmpVar = 'feifeiyu'
console.log('tmpVar1:', tmpVar) // => tmpVar1: feifeiyu
}())

console.log('tmpVar1:', tmpVar) // => Uncaught ReferenceError: tmpVar is not defined
</script>
<script>
console.log('tmpVar2:', tmpVar) // => Uncaught ReferenceError: tmpVar is not defined
</script>

避免无意中引入全局变量

在 JavaScript 中如果一个变量未使用关键字 var 进行定义, 那么该变量就默认是属于全局变量的, 在开发中应当杜绝使用未定义的变量, 在开发中可以使用 ES5 的 strict 模式进行开发, 在这种模式下, 没有声明的变量在运行中会抛出错误 参考: Javascript 严格模式详解
下面罗列几种常见的无意中引入代码的方式

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
<script>
function foo(x, y) {
//未使用 var 关键字, res1属于全局变量
res1 = x + y
// 使用了 var 关键字, res2属于局部变量
var res2 = x + y
console.log('res1 in func:', res1) // => res1: 3
console.log('res2 in func:', res2) // => res2: 3
}
foo(1, 2)
console.log('res1 out func:', res1) // => res1: 3
console.log('res2 out func:', res2) // => Uncaught ReferenceError: res2 is not defined
</script>
<script>
function bar() {
// res3 属于局部变量
// res4 属于全局变量
// 这种表达式的声明顺序是 先声明 res4 = 3, 然后 var res3 = res4
var res3 = res4 = 3
//下面这种就是都是全局变量了
res5 = res6 = 3
console.log('res3 in func:', res3) // => res3 in func: 3
console.log('res4 in func:', res4) // => res4 in func: 3
console.log('res5 in func:', res5) // => res5 in func: 3
console.log('res6 in func:', res6) // => res5 in func: 3
}
bar()
console.log('res4 out func:', res4) // => res4 out func: 3
console.log('res5 in func:', res5) // => res5 out func: 3
console.log('res6 in func:', res6) // => res6 out func: 3
console.log('res3 in func:', res3) // => Uncaught ReferenceError: res3 is not defined
</script>

var 声明的副作用

使用 var 关键字声明的全局变量与直接定义的变量有细微不同:

  • 使用 var 声明的全局变量无法使用 delete 删除
  • 不使用 var 声明的全局变量可以使用 delete 删除 (这样声明的变量是全局对象的一个属性)

关注变量提升

变量提升是 JavaScript 代码编写中容易忽略的一个点。JavaScript 允许在函数任意地方声明多个变量, 无论在哪里声明, 效果都等同于在函数顶部声明, 这就是变量提升(hoisting)。当先使用变量后定义变量,将会导致逻辑上的错误。例如:

1
2
3
4
5
6
7
8
(function() {
var myName = 'feifeiyu'
function func() {
console.log('myName 1:', myName) // => myName 1: undefined
var myName = 'feiyu'
console.log('myName 2:', myName) // => myName 2: feiyu
}
}())

上述代码执行后, 安装开发者的意图, 第一个console 输出的应该是 myName 1: feifeiyu, 第二个 console 输出的应该是 feiyu, 然而意想不到的结果出现了, 第一个 console 输出的是 undefined。 导致这个的元凶是 JavaScript 的变量提升, JavaScript 解释器在执行时, 函数中的 myName 会提升到函数 func 最顶部去定义, 本质上述代码是按如下逻辑去执行的:

1
2
3
4
5
6
7
8
9
(function() {
var myName = 'feifeiyu'
function func() {
var myName //此时 myName 是 undefined
console.log('myName 1:', myName) // => myName 1: undefined
myName = 'feiyu'
console.log('myName 2:', myName) // => myName 2: feiyu
}
}())

函数在定义的时候也存在变量提升的问题, 而且函数提升的优先级高于变量

1
2
3
4
5
6
7
8
9
10
11
(function() {
bar() // => func bar: 3
var foo = 2
function foo() {
console.log('func foo: 2')
}
console.log('foo', foo) // => foo 2
function bar() {
console.log('func bar: 3')
}
}())

上例中, bar 函数使用在前, 定义在后, 但 bar() 函数能正常输出。
然而,按照正常逻辑来说, 输出的是 foo 函数 function foo() { console.log(‘output: 2’) }, 然而实际输出的却是 foo 2, 由此可以推断出, 上面例子中 JavaScript 解释器执行逻辑是:先初始化 foo 函数, 再初始化 foo 变量, 导致 foo 函数被 foo 变量覆盖。如下面示例代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function() {
//定义
function foo() {
console.log('func foo: 2')
}
function bar() {
console.log('func bar: 3')
}
var foo
//运行 bar
bar()
//foo 赋值
foo = 2
//执行
console.log('foo', foo) // => foo 2
}())

按照正常的程序编写逻辑来说, 函数或者变量一般都应该遵循先定义后使用的习惯, 如何避免函数或变量在定义之前被使用, 对于变量来说,在定义之前使用,变量值都是 undefined 调试过程中容易发现; 对于函数来说,可以使用如下方式进行定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function() {
// bar() Uncaught TypeError: bar is not a function
var bar = function () {
console.log('bar func: 3')
}
bar() // => bar func: 3
}())
//解释器执行逻辑
(function() {
var bar
// bar() Uncaught TypeError: bar is not a function
bar = function () {
console.log('bar func: 3')
}
bar() // => bar func: 3
}())

从上例可以看出, 采用将函数赋值给一个变量的形式, 可以避免函数先使用后定义的情况发生。推荐在开发过程中都采用这种方式定义函数。
此外推荐一篇关于变量提升的博客: 一道常被人轻视的前端JS面试题

for 循环的使用

在 JavaScript 中有两种 for 循环, 分别是 for…in… 和 for…, 他们分别主要用于对象和数组。下面分别介绍这两种循环方式

for… 循环

for… 循环与其他语言(Java, C++) 中的 for 循环用法一样。

1
2
3
4
5
(function() {
for(var i=0; i < array.length; i++) {
// handle array[i]
}
}())

采用上述模式有一个缺陷, 就是每一次循环都要计算一次, array.length, 这样会大大拖慢浏览器执行速度。比较好的方式是只计算一次数组长度,将这个长度值保存起来, for 循环根据这个值设定循环次数。

1
2
3
4
5
6
7
(function() {
var array = [ 1, 2, 3, 4]
var max = array.length
for(var i = 0; i < max; i++) {
console.log(array[i])
}
}())

for…in.. 循环

for…in…循环是设计用来循环 JavaScript 对象的, 也可以称为枚举(emumeration)

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
(function() {
var student = {
name: 'feifeiyu',
age: '25',
gender: 'male'
}
for (var item in student) {
console.log(item)
}

//output keys
// => name
// => age
// => gender

//采用构造函数创建对象
var Employee = function() {
this.name = 'feifieyu'
this.age = 25
this.gender = 'male'
}
//原型链添加的对象
Employee.prototype.company = 'tianzhu'
var emp = new Employee()
for(var item in emp) {
console.log(item)
}
// output keys
// => name
// => age
// => gender
// => company //通过原型链添加的属性,可以遍历出来
}())

从上例可以看出, 采用 for…in… 遍历对象属性不仅可以遍历出对象自身的属性, 也可以遍历出原型添加的属性。 这也是为什么不推荐在遍历数组的时候使用 for…in..。
如果要过滤原型链添加的属性, 可以采用 hasOwnProperty 来判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Employee = function() {
this.name = 'feifieyu'
this.age = 25
this.gender = 'male'
}
//原型链添加的对象
Employee.prototype.company = 'tianzhu'
var emp = new Employee()
for(var item in emp) {
if(emp.hasOwnProperty(item)) {
console.log(item)
}
// output keys
// => name
// => age
// => gender
//通过原型链添加的属性被过滤
}

下面解释下为什么不用 for…in… 过滤数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Array.prototype.tag = 'test'
var arr = [1,2,3]
for(var item in arr) {
console.log(item)
}
//output
// => 1
// => 2
// => 3
// => tag //原型链的属性也被打印出来
var max = arr.length
for(var i = 0; i < max; i++) {
console.log(arr[i])
}
//output
// => 1
// => 2
// => 3

从上例中可以看出, 如果对 Array 对象添加了一个原型属性, 采用 for…in… 遍历时会将该属性名也加入遍历, 这不是我们所设想的。所以为了安全起见, 不采用 for…in… 遍历数组, 采用普通的for…循环遍历。

内置构造函数原型

在 JavaScript 中常见的内置构造函数 Object(), Array(), Function(), String()等,这些构造函数可以通过原型链添加属性,上面例子中就有涉及, 通过 Array.prototype 可以添加新的属性,并影响到所有的数组创建。这种方式虽然十分灵活强大,但会严重影响代码的可维护行, 将代码变得不可预测。
不过对于 Function() 对象作为构造函数时,常使用 prototype 增加对象属性, 同其他高级语言的继承属性类似, 在 JavaScript 组件开发中常用。

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
<script>
(function() {
Object.prototype.hello = function(name) {
return 'hello' + name
}
var obj = {name: 'feifeiyu', age: 25}
for(key in obj) {
console.log('obj1:', key)
}
// output
// => obj1: name
// => obj1: age
// => obj1: hello
}())
</script>
<script>
(function() {
var obj2 = {name: 'feifeiyu', age: 25}
for(key in obj2) {
console.log('obj2:', key)
}
// output
// => obj2: name
// => obj2: age
// => obj2: hello
}())
</script>

例如: 上面代码中,通过 Object.prototype 添加的属性, 这个影响是全局的, 杀伤面积过大, 太过流氓, 尽量慎用。
这种方式主要用于浏览器的兼容处理 -> polyfill。例如为不支持 bind 方法的低版本浏览器添加该方法。参考 MDN 的 function bind polyfill

避免隐式类型转换

JavaScript 在执行比较语句时,会执行隐式的类型转换, 例如: false == ‘’ 或 false == 0 或 null == undefined 判断都为 true。为了避免隐式的类型转换导致的逻辑混乱, 在使用比较语句时, 使用 === 或 !== 来进行比较, 这种比较相对严格, 会校验比较对象的数据类型,不同类型比较对象返回 false。

避免使用 eval()

在 JavaScript 中是: “eval() is a devil”, 该函数可以将任意字符串当做一个 JavaScript 脚本来执行。 会导致一些安全隐患, hack 只需要将一段 JavaScript 脚本字符串插入 eval() 中既可执行。 eval() 在任何场景下都应该禁用。

END