随着前端模块化技术的引入, 前端开发越来越趋向于后端的开发模式, 一个页面的 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
18var 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
18Array.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() 在任何场景下都应该禁用。