在我们日常编码中,编写可维护代码是非常重要的,编写可维护代码,意味着:

  • 可读性
  • 一致性
  • 可预测性
  • 团队代码风格一致

最小全局变量

JavaScript 是通过函数来管理作用域的。在函数内部申明的变量只能在函数内部使用,在函数外部不能使用。每一个 JavaScript 都有一个全局对象,当你在任意的函数外部访问 this 的时候可以访问到,你创建的每一个全局变量都成为这个全局对象的属性。浏览器中,这个全局对象便是 window。

myglobal = 'Hello';
console.log(myglobal);
console.log(window.myglobal);
console.log(window['myglobal']);
console.log(this.myglobal);

全局变量的问题

全局变量最大的问题就是变量名冲突,造成不可以预计的后果。而在 web 页面中包含不是开发者缩写的代码也很常见

  • 第三方 JavaScript 库
  • 广告或者统计脚本
  • 不同类型的组件

所以一旦出现命名冲突,可能会造成不可预估的错误。所以尽可能的少用全局变量是非常有必要的。比如使用局部变量声明,es6 中的letconst,这里我们主要探讨下 var。

由于 JavaScript 的两个特征,不自觉的创建变量是出乎意料的容易,首先,你可以甚至不需要声明就可以使用变量;第二,JavaScript 有隐含的全局概念,意味着你不声明的任何变量都会成为一个全局对象属性。

function sum(x, y) {
  // 不推荐写法: 隐式全局变量
  result = x + y;
  return result;
}

另一个创建隐式全局变量的反例就是使用任务链进行部分 var 声明。下面的片段中,a 是本地变量但是 b 却是全局变量,这可能不是你希望发生的

// 反例,勿使用
function foo() {
  var a = (b = 0);
  // ...
}

忘记 var 的副作用

隐式全局变量和明确定义的全局变量间有些小的差异,就是通过delete操作符让变量未定义的能力。

  • 通过 var 创建的全局变量(任何函数之外的程序中创建)是不能被删除的
  • 隐式全局变量(无视是否在函数中创建)是能被删除的

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

单 var 形式

在函数顶部使用单 var 语句是比较有用的一种形式,其好处在于

  • 提供了一个单一的地方去寻找功能所需要的所有局部变量
  • 防止变量在定义之前使用的逻辑错误
  • 帮助你记住声明的全局变量,因此较少了全局变量
  • 少代码(类型、传值、单线完成)
function func() {
  var a = 1,
    b = 2,
    sum = a + b,
    myobject = {},
    i,
    j;
  // function body...
}

预解析:var 散布的问题

JavaScript 中,你可以在函数的任何位置声明多个 var 语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于 JavaScript,只要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在 var 声明前使用的时候。

// 反例
myname = 'global'; // 全局变量
function func() {
  alert(myname); // "undefined"
  var myname = 'local';
  alert(myname); // "local"
}
func();

由于函数声明提前,也就是预解析。为了避免这种混乱,最好是预先声明你想使用的全部变量。

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

for 循环

推荐写法

for (var i = 0, max = myarray.length; i < max; i++) {
  // 使用myarray[i]做点什么
}

伴随着单 var 形式,你可以把变量从循环中提出来,就像下面这样:

function looper() {
  var i = 0,
    max,
    myarray = [];
  // ...
  for (i = 0, max = myarray.length; i < max; i++) {
    // 使用myarray[i]做点什么
  }
}

最后一个需要对循环进行调整的是使用下面表达式之一来替换i++

i = i + 1;
i += 1;

++–-促进了“过分棘手(excessive trickiness)”。

还有两种变化的形式,其又有了些微改进,因为:

  • 少了一个变量(无 max)
  • 向下数到 0,通常更快,因为和 0 做比较要比和数组长度或是其他不是 0 的东西作比较更有效率
//第一种变化的形式:

var i, myarray = [];
for (i = myarray.length; i– - ;) {
  // 使用myarray[i]做点什么
}

//第二种使用while循环:

var myarray = [],
  i = myarray.length;
while (i– - ) {
  // 使用myarray[i]做点什么
}

for-in Loops

for-in 循环应该用在非数组对象的遍历上,使用 for-in 进行循环也被称为“枚举”

从技术上将,你可以使用 for-in 循环数组(因为 JavaScript 中数组也是对象),但这是不推荐的。因为如果数组对象已被自定义的功能增强,就可能发生逻辑错误。另外,在 for-in 中,属性列表的顺序(序列)是不能保证的。所以最好数组使用正常的 for 循环,对象使用 for-in 循环。

有个很重要的hasOwnProperty()方法,当遍历对象属性的时候可以过滤掉从原型链上下来的属性。

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

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

在这个例子中,我们有一个使用对象字面量定义的名叫 man 的对象。在 man 定义完成后的某个地方,在对象原型上增加了一个很有用的名叫 clone()的方法。此原型链是实时的,这就意味着所有的对象自动可以访问新的方法。为了避免枚举 man 的时候出现clone()方法,你需要应用hasOwnProperty()方法过滤原型属性。如果不做过滤,会导致clone()函数显示出来,在大多数情况下这是不希望出现的。

// 1.
// for-in 循环
for (var i in man) {
  if (man.hasOwnProperty(i)) {
    // 过滤
    console.log(i, ':', man[i]);
  }
}
/* 控制台显示结果
hands : 2
legs : 2
heads : 1
*/
// 2.
// 反面例子:
// for-in loop without checking 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]);
  }
}

其好处在于在 man 对象重新定义hasOwnProperty情况下避免命名冲突。也避免了长属性查找对象的所有方法,你可以使用局部变量“缓存”它。

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

(不)扩展内置原型

增加内置的构造函数原型(如Object(), Array(), 或Function())挺诱人的,但是这严重降低了可维护性,因为它让你的代码变得难以预测。使用你代码的其他开发人员很可能更期望使用内置的 JavaScript 方法来持续不断地工作,而不是你另加的方法。

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。为避免引起混乱的隐含类型转换,在你比较值和表达式类型的时候始终使用===!==操作符。

避免 eval()

如果代码是在运行时动态生成,有一个更好的方式不使用 eval 而达到同样的目标。例如,用方括号表示法来访问动态属性会更好更简单:

// 反面示例
var property = 'name';
alert(eval('obj.' + property));

// 更好的
var property = 'name';
alert(obj[property]);

使用eval()也带来了安全隐患,因为被执行的代码(例如从网络来)可能已被篡改。这是个很常见的反面教材,当处理 Ajax 请求得到的 JSON 相应的时候。在这些情况下,最好使用 JavaScript 内置方法来解析 JSON 相应,以确保安全和有效。若浏览器不支持JSON.parse(),你可以使用来自JSON.org的库。

同样重要的是要记住,给setInterval(), setTimeout()Function()构造函数传递字符串,大部分情况下,与使用eval()是类似的,因此要避免。在幕后,JavaScript 仍需要评估和执行你给程序传递的字符串:

// 反面示例
setTimeout('myFunc()', 1000);
setTimeout('myFunc(1, 2, 3)', 1000);

// 更好的
setTimeout(myFunc, 1000);
setTimeout(function() {
  myFunc(1, 2, 3);
}, 1000);

如果你必须使用eval(),可以考虑使用new Function()代替。有一个小的潜在好处,因为在new Function()中作代码评估是在局部函数作用域中运行,所以代码中任何被评估的通过 var 定义的变量都不会自动变成全局变量。另一种方法来阻止自动全局变量是封装eval()调用到一个即时函数中。

console.log(typeof un); // "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

var jsstring = 'var un = 1; console.log(un);';
eval(jsstring); // 1

jsstring = 'var deux = 2; console.log(deux);';
new Function(jsstring)(); // 2

jsstring = 'var trois = 3; console.log(trois);';
(function() {
  eval(jsstring);
})(); // 3

console.log(typeof un); // number
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"

另一间eval()Function构造不同的是eval()可以干扰作用域链,而Function()则不会。不管你在哪里执行 Function(),它只看到全局作用域。所以其能很好的避免本地变量污染。

parseInt 下的数值转换

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

空格

空格的使用同样有助于改善代码的可读性和一致性。在写英文句子的时候,在逗号和句号后面会使用间隔。在 JavaScript 中,你可以按照同样的逻辑在列表模样表达式(相当于逗号)和结束语句(相对于完成了“想法”)后面添加间隔。

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

  • 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 () {};

使用空格分开所有的操作符和操作对象是另一个不错的使用,这意味着在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=等前后都需要空格。

注释、 驼峰命名