随着前端模块化技术的引入, 前端开发越来越趋向于后端的开发模式, 一个页面的 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 函数的作用域内可以访问
由上例中想到, 如果一个页面中如果有两块独立的代码中, 包含有相同名称的变量, 但却有着不同的作用时, 会引起程序运行出错。在其他的程序中为了解决这个问题引入了命名空间的概念, 例如: C++ 有个专门的关键字 namespace 来声明命名空间。在 JavaScript 中没这样的关键字来控制命名空间, 因此, 为了避免变量在全局作用域中冲突, 在同一个页面中应该尽可能地减少全局作用域中的使用。如何减少全局变量数量, 大致的解决方案有两种, 一种是人为去构造 JavaScript 的命名空间, 这种模式相对复杂不常用; 另一种是采用自执行即使函数(self-execting immediate funtionsl)来实现。
自执行立即函数
采用自执行立即函数实现减少全局变量名的方式本质上还是,利用 JavaScript 的函数作用域来实现, 例如下面代码中定义的 tmpVar 只能在自执行立即函数中有效, 而在其他代码块中无效。 因此, 在页面开发时可以使用自执行立即函数来封装不同的代码, 不同代码中定义的变量都只在相应的自执行立即函数中有效, 从而减少全局变量的定义。 这个在 JavaScript 组件开发中常用。
避免无意中引入全局变量
在 JavaScript 中如果一个变量未使用关键字 var 进行定义, 那么该变量就默认是属于全局变量的, 在开发中应当杜绝使用未定义的变量, 在开发中可以使用 ES5 的 strict 模式进行开发, 在这种模式下, 没有声明的变量在运行中会抛出错误 参考: Javascript 严格模式详解。
下面罗列几种常见的无意中引入代码的方式
var 声明的副作用
使用 var 关键字声明的全局变量与直接定义的变量有细微不同:
- 使用 var 声明的全局变量无法使用 delete 删除
- 不使用 var 声明的全局变量可以使用 delete 删除 (这样声明的变量是全局对象的一个属性)
关注变量提升
变量提升是 JavaScript 代码编写中容易忽略的一个点。JavaScript 允许在函数任意地方声明多个变量, 无论在哪里声明, 效果都等同于在函数顶部声明, 这就是变量提升(hoisting)。当先使用变量后定义变量,将会导致逻辑上的错误。例如:
上述代码执行后, 安装开发者的意图, 第一个console 输出的应该是 myName 1: feifeiyu, 第二个 console 输出的应该是 feiyu, 然而意想不到的结果出现了, 第一个 console 输出的是 undefined。 导致这个的元凶是 JavaScript 的变量提升, JavaScript 解释器在执行时, 函数中的 myName 会提升到函数 func 最顶部去定义, 本质上述代码是按如下逻辑去执行的:
函数在定义的时候也存在变量提升的问题, 而且函数提升的优先级高于变量
上例中, bar 函数使用在前, 定义在后, 但 bar() 函数能正常输出。
然而,按照正常逻辑来说, 输出的是 foo 函数 function foo() { console.log(‘output: 2’) }, 然而实际输出的却是 foo 2, 由此可以推断出, 上面例子中 JavaScript 解释器执行逻辑是:先初始化 foo 函数, 再初始化 foo 变量, 导致 foo 函数被 foo 变量覆盖。如下面示例代码所示:
按照正常的程序编写逻辑来说, 函数或者变量一般都应该遵循先定义后使用的习惯, 如何避免函数或变量在定义之前被使用, 对于变量来说,在定义之前使用,变量值都是 undefined 调试过程中容易发现; 对于函数来说,可以使用如下方式进行定义
从上例可以看出, 采用将函数赋值给一个变量的形式, 可以避免函数先使用后定义的情况发生。推荐在开发过程中都采用这种方式定义函数。
此外推荐一篇关于变量提升的博客: 一道常被人轻视的前端JS面试题
for 循环的使用
在 JavaScript 中有两种 for 循环, 分别是 for…in… 和 for…, 他们分别主要用于对象和数组。下面分别介绍这两种循环方式
for… 循环
for… 循环与其他语言(Java, C++) 中的 for 循环用法一样。
采用上述模式有一个缺陷, 就是每一次循环都要计算一次, array.length, 这样会大大拖慢浏览器执行速度。比较好的方式是只计算一次数组长度,将这个长度值保存起来, for 循环根据这个值设定循环次数。
for…in.. 循环
for…in…循环是设计用来循环 JavaScript 对象的, 也可以称为枚举(emumeration)
从上例可以看出, 采用 for…in… 遍历对象属性不仅可以遍历出对象自身的属性, 也可以遍历出原型添加的属性。 这也是为什么不推荐在遍历数组的时候使用 for…in..。
如果要过滤原型链添加的属性, 可以采用 hasOwnProperty 来判断:
下面解释下为什么不用 for…in… 过滤数组
从上例中可以看出, 如果对 Array 对象添加了一个原型属性, 采用 for…in… 遍历时会将该属性名也加入遍历, 这不是我们所设想的。所以为了安全起见, 不采用 for…in… 遍历数组, 采用普通的for…循环遍历。
内置构造函数原型
在 JavaScript 中常见的内置构造函数 Object(), Array(), Function(), String()等,这些构造函数可以通过原型链添加属性,上面例子中就有涉及, 通过 Array.prototype 可以添加新的属性,并影响到所有的数组创建。这种方式虽然十分灵活强大,但会严重影响代码的可维护行, 将代码变得不可预测。
不过对于 Function() 对象作为构造函数时,常使用 prototype 增加对象属性, 同其他高级语言的继承属性类似, 在 JavaScript 组件开发中常用。
例如: 上面代码中,通过 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() 在任何场景下都应该禁用。