编写高质量JavaScript代码

本文知识点是摘自于汤姆大叔博客,自己记录理解并学习

书写可维护的代码

软件开发是一个熵增的过程,随着软件结构越来越复杂,代码维护就变得举步维艰,当你发现 bug 并理解修复他是最好的。否则,等你转移到其他任务,一段时间后再回来看你的代码,就需要:

  • 花时间学习和理解问题
  • 花时间学习和理解解决问题的代码

如果是特别大的公司,开发人员并不是同一个人。因此,必须降低理解代码的成本,我们应该去开发更激动人心的功能,而不是花几小时几天去维护遗留代码。

随着应用功能的增强,我们也许会面临以下问题:

  • bug 是暴露的
  • 要添加新功能
  • 应用可能要适应新的环境(比如新的浏览器)
  • 应用要改变用途
  • 甚至要重新再来(换一种架构或是用另一种语言编写)

由于这些变化,我们不得不编写高质量,高性能,可维护的代码以减小我们维护的成本。

这意味着我们的代码要:

  • 可读的
  • 一致的
  • 可预测的
  • 看上去是同一个人写的(???)
  • 已记录

全局变量的问题

全局变量的问题意味着你的应用和页面上的所有代码都共享这些全局变量,如果遇到同名但是不同功能的变量,冲突在所难免。
可能遇到的变量冲突的情况:

  • 第三方 js 库
  • 不同的人写的代码

假如有的第三方库里面有的叫做 apply 的方法,而你又定义了一个 apply 的变量,那这个第三方库的 apply 就嗝屁了!

由于 JavaScript 的特性,我们不自觉的就会创建出全局变量,首先,你可以甚至不需要声明就可以使用变量,第二,JavaScript 有隐含的全局概念,意味着你不声明的任何变量都会成为全局变量:

function() {
  a = "1";
}
console.log(window.a); // "1"

此段代码中 a 并没有声明,但是莫名出现了一个全局变量 a,解决方法是始终使用var声明变量:

function() {
  var a = 1;
}
consloe.log(window.a); // "undefined"

另一种反例就是使用任务链的情况下:

function() {
  var a = b = "1";
}
console.log(a, b); // "undefined" "1"

看起来变量 b 又成为了全局变量,这并不是我们想要的,最理想的方法是使用“,”分隔进行声明,并且可以做一些基础操作:

function() {
  var a, b = "1";
}
function() {
  var a = 1, b = 2, sum = a + b;
}
console.log(a, b); // "undefined" "undefined"

这样的好处:

  • 提供了单一的地方寻找变量
  • 防止变量在声明之前使用

这样我们就避免了全局变量污染。

在 ES5 严格模式下未声明变量就使用会抛出一个错误

在 ES6 中我们可以使用let关键字声明避免全局变量污染

隐式全局变量和明确定义全局变量还是有一点区别的:

  • 通过var定义的全局变量不能通过delete操作符删除
  • var定义的全局变量可以通过delete操作符删除

这表明,在技术上,隐式全局变量并不是真正的全局变量,但它们是全局对象的属性。属性是可以通过 delete 操作符删除的,而变量是不能的:

var global_var = "1";
global_novar = "2";
(function () {
  global_fromfun = "3";
})();

// 试图删除
delete global_var;
delete global_novar;
delete global_fromfun;

// 测试
typeof global_var; // "string"
typeof global_novar; // "undefined"
typeof global_fromfun; // "undefined"

预解析(hoisting)

在 JavaScript 中,你可以在函数的任何位置声明变量,执行起来并无差异,这是因为函数在执行时的预解析,讲变量提升到该作用域顶部:

var a = "2";
function() {
  alert(a); // "undefined"
  var a = "1";
  alert(a); // "1"
}

在这个例子中,你会以为第一个 alert 会是“2”,实际上并非如此,因为函数预解析时检查到当前作用域内有一个变量 a,所以将它提升到当前作用域的顶部,类似于下面这个例子:

var a = "2";
function() {
  var a;
  alert(a); // "undefined"
  a = "1";
  alert(a); // "1"
}

为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种 ECMAScript 标准中并未定义,通常用来描述行为。

for 循环

通常的循环形式如下:

for (var i = 0; i < arr.length; i++) {
  //
}

这种循环的不足之处在于每次循环都要去取数组的长度,这会降低代码的执行效率,尤其当你的数组不是一个纯数组,而是一个 HTMLCollection 的时候,这意味着你每次循环都会去查询 DOM,而查询 DOM 是相当费时的。

这就是为什么要缓存数组长度的原因:

for (var i = 0, arrLength = arr.length; i < arrLength; i++) {
  //
}

在此过程中,你只检索了一次长度。

现如今 V8 引擎已相当强大,如果你是普通数组,他会在编译时把你确定不变的代码移到循环外,其实差距不大,如果涉及到 NodeLiist,建议还是要缓存一下。

如果你还在纠结,也可以写成下面这个样子:

for (var i = arr.length; i > 0; i--) {
  //
}

for-in 循环

for-in 循环主要用在非数组对象的遍历上,也被称为“枚举”。

从技术上讲,for-in 也可以循环数组,但这是不推荐的,因为如果对象数组被自定义的功能增强,就可能发生逻辑错。而且,for-in 循环顺序是不能保证的。

有一个很重要的方法:hasOwnProperty(),这个方法的作用是判断是不是对象本身的属性,可用于去除原型链上的属性:

// 对象
var man = {
   hands: 2,
   legs: 2,
   heads: 1
};

// 在代码的某个地方
// 一个方法添加给了所有对象
if (typeof Object.prototype.clone === "undefined") {
   Object.prototype.clone = function () {};
}

// 有hasOwnProperty方法
for (var i in man) {
   if (man.hasOwnProperty(i)) { // 过滤
      console.log(i, ":", man[i]);
   }
}
//  控制台显示结果
hands : 2
legs : 2
heads : 1

// 反例
// 无hasOwnProperty方法
for (var i in man) {
   console.log(i, ":", man[i]);
}
// 控制台显示结果
hands : 2
legs : 2
heads : 1
clone: function() // 原型链上的方法被“枚举”出来

另外一种使用 hasOwnProperty()的形式是取消 Object.prototype 上的方法。像是:

for (var i in man) {
  if (Object.prototype.hasOwnProperty.call(man, i)) {
    // 过滤
    console.log(i, ":", man[i]);
  }
}

严格来说,不使用 hasOwnProperty()并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加 hasOwnProperty()更加保险些。

Switch

你可以通过类似下面形式的 switch 语句增强可读性和健壮性:

var inspect_me = 0,
  result = "";
switch (inspect_me) {
  case 0:
    result = "zero";
    break;
  case 1:
    result = "one";
    break;
  default:
    result = "unknown";
}

这个简单的例子中所遵循的风格约定如下:

  • 每个 case 和 switch 对齐(花括号缩进规则除外)
  • 每个 case 中代码缩进
  • 每个 case 以 break 清除结束
  • 避免贯穿(故意忽略 break)。如果你非常确信贯穿是最好的方法,务必记录此情况,因为对于有些阅读人而言,它们可能看起来是错误的。
  • 以 default 结束 switch:确保总有健全的结果,即使无情况匹配。

避免隐式类型转换

JavaScript 的变量在比较的时候会隐式类型转换。这就是为什么一些诸如:false == 0 或 “” == 0 返回的结果是 true。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用=和!操作符。

var zero = 0;
if (zero === false) {
  // 不执行,因为zero为0, 而不是false
}

// 反面示例
if (zero == false) {
  // 执行了...
}

有的观点认为这是没有必要的。然而 JSlint 要求严格相等,因为你可以清楚的看到变量的类型和值,以降低代码阅读时的精力消耗,不需要再去纠结“==”是故意的还是一个疏漏。

避免 eval()

此方法接受任意字符串作为参数,并作为 JavaScript 来处理,,使用 eval()也带来了安全隐患,因为被执行的代码(例如从网络来)可能已被篡改

“eval 是魔鬼”

如果你绝对必须使用 eval(),你可以考虑使用 new Function()代替,有几个好处是代码将会在函数的局部作用域执行,并且不会污染全局变量

考虑下面这个例子,这里仅 a 作为全局变量污染了命名空间:

console.log(typeof a); // "undefined"
console.log(typeof b); // "undefined"
console.log(typeof c); // "undefined"

var jsstring = "var a = 1; console.log(a);";
eval(jsstring); // logs "1"

jsstring = "var b = 2; console.log(b);";
new Function(jsstring)(); // logs "2"

jsstring = "var c = 3; console.log(c);";
(function () {
  eval(jsstring);
})(); // logs "3"

console.log(typeof a); // number
console.log(typeof b); // "undefined"
console.log(typeof c); // "undefined"

parseInt()下的数值转换

使用 parseInt()你可以从字符串中获取数值,该方法接受另一个基数参数,这经常省略,但不应该。当字符串以“0”开头的时候就有可能会出问题,例如,部分时间进入表单域,在 ECMAScript 3 中,开头为”0″的字符串被当做八进制处理了,但这已在 ECMAScript 5 中改变了。为了避免矛盾和意外的结果,总是指定基数参数。

var month = "06",
  year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);

编码规范

建立和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队可以通读规范,理解其它团队成员书写的代码,更快上手干活。

要记住,建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。

缩进

代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,因为它看上去像是遵循了规范,但是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。

使用 Tab 和空格都无所谓,最重要的是保持一致!

空格

适合使用空格的地方包括:

  • for 循环分号分开后的的部分:如 for (var i = 0; i < 10; i += 1)
  • for 循环中初始化的多变量(i 和 max):for (var i = 0, max = 10; i < max; i += 1)
  • 分隔数组项的逗号的后面:var a = [1, 2, 3];
  • 对象属性逗号的后面以及分隔属性名和属性值的冒号的后面:var o = {a: 1, b: 2};
  • 限定函数参数:myFunc(a, b, c)
  • 函数声明的花括号的前面:function myFunc() {}
  • 匿名函数表达式 function 的后面:var myFunc = function () {};

空格使用的一点不足就是增加了文件的大小,但是压缩无此问题。

命名规范

  • 大驼峰式命名法命名构造函数:new Person()
  • 小驼峰式命名法命名函数和方法:getFirstName()
  • 全大写字母命名常量:var PI = 3.14
  • _开头命名私有变量:_getFirst: function()

还有另外一个完全大写的惯例:全局变量名字全部大写。全部大写命名全局变量可以加强减小全局变量数量的实践,同时让它们易于区分。

注释

你必须注释你的代码,即使不会有其他人向你一样接触它。通常,当你深入研究一个问题,你会很清楚的知道这个代码是干嘛用的,但是,当你一周之后再回来看的时候,想必也要耗掉不少脑细胞去搞明白到底怎么工作的。

最重要的习惯,然而也是最难遵守的,就是保持注释的及时更新,因为过时的注释比没有注释更加的误导人。

posted @ 2025-11-17 18:01  jinzhepro  阅读(8)  评论(0)    收藏  举报