全局变量的问题 全局变量的问题在于,你的JavaScript应用程序和web页面上的所有代码都共享了这些全局变量,他们住在同一个全局命名空间,所以当程序的两个不同部分定义同名但不同作用的全局变量的时候,命名冲突在所难免。 web页面包含不是该页面开发者所写的代码也是比较常见的,例如: 第三方的JavaScript库 广告方的脚本代码 第三方用户跟踪和分析脚本代码 不同类型的小组件,标志和按钮 比方说,该第三方脚本定义了一个全局变量,叫做result;接着,在你的函数中也定义一个名为result的全局变量。其结果就是后面的变量覆盖前面的,第三方脚本就一下子嗝屁啦! 因此,要想和其他脚本成为好邻居的话,尽可能少的使用全局变量是很重要的。在书中后面提到的一些减少全局变量的策略,例如命名空间模式或是函数立即自动执行,但是要想让全局变量少最重要的还是始终使用var来声明变量。 由于JavaScript的两个特征,不自觉地创建出全局变量是出乎意料的容易。首先,你可以甚至不需要声明就可以使用变量;第二,JavaScript有隐含的全局概念,意味着你不声明的任何变量都会成为一个全局对象属性。参考下面的代码: 复制代码 代码如下: function sum(x, y) { // 不推荐写法: 隐式全局变量 result = x + y; return result; }
此段代码中的result没有声明。代码照样运作正常,但在调用函数后你最后的结果就多一个全局命名空间,这可以是一个问题的根源。 经验法则是始终使用var声明变量,正如改进版的sum()函数所演示的: 复制代码 代码如下: function sum(x, y) { var result = x + y; return result; }
另一个创建隐式全局变量的反例就是使用任务链进行部分var声明。下面的片段中,a是本地变量但是b确实全局变量,这可能不是你希望发生的: 复制代码 代码如下: // 反例,勿使用 function foo() { var a = b = 0; // ... }
此现象发生的原因在于这个从右到左的赋值,首先,是赋值表达式b = 0,此情况下b是未声明的。这个表达式的返回值是0,然后这个0就分配给了通过var定义的这个局部变量a。换句话说,就好比你输入了: var a = (b = 0); 如果你已经准备好声明变量,使用链分配是比较好的做法,不会产生任何意料之外的全局变量,如: 复制代码 代码如下: function foo() { var a, b; // ... a = b = 0; // 两个均局部变量 }
这种方法可以随时获得全局对象,因为其在函数中被当做函数调用了(不是通过new构造),this总 是指向全局对象。实际上这个病不适用于ECMAScript 5严格模式,所以,在严格模式下时,你必须采取不同的形式。例如,你正在开发一个JavaScript库,你可以将你的代码包裹在一个即时函数中,然后从 全局作用域中,传递一个引用指向this作为你即时函数的参数。 单var形式(Single var Pattern) 在函数顶部使用单var语句是比较有用的一种形式,其好处在于: 提供了一个单一的地方去寻找功能所需要的所有局部变量 防止变量在定义之前使用的逻辑错误 帮助你记住声明的全局变量,因此较少了全局变量//zxx:此处我自己是有点晕乎的… 少代码(类型啊传值啊单线完成) 单var形式长得就像下面这个样子: 复制代码 代码如下: function func() { var a = 1, b = 2, sum = a + b, myobject = {}, i, j; // function body... }
您可以使用一个var语句声明多个变量,并以逗号分隔。像这种初始化变量同时初始化值的做法是很好的。这样子可以防止逻辑错误(所有未初始化但声明的变量的初始值是undefined)和增加代码的可读性。在你看到代码后,你可以根据初始化的值知道这些变量大致的用途,例如是要当作对象呢还是当作整数来使。 你也可以在声明的时候做一些实际的工作,例如前面代码中的sum = a + b这个情况,另外一个例子就是当你使用DOM(文档对象模型)引用时,你可以使用单一的var把DOM引用一起指定为局部变量,就如下面代码所示的: 复制代码 代码如下: function updateElement() { var el = document.getElementById("result"), style = el.style; // 使用el和style干点其他什么事... }
预解析:var散布的问题(Hoisting: A Problem with Scattered vars) JavaScript中,你可以在函数的任何位置声明多个var语句,并且它们就好像是在函数顶部声明一样发挥作用,这种行为称为 hoisting(悬置/置顶解析/预解析)。当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只 要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。看下面这个例子: 复制代码 代码如下: // 反例 myname = "global"; // 全局变量 function func() { alert(myname); // "undefined" var myname = "local"; alert(myname); // "local" } func();
在这个例子中,你可能会以为第一个alert弹出的是”global”,第二个弹出”loacl”。这种期许是可以理解的,因为在第一个alert 的时候,myname未声明,此时函数肯定很自然而然地看全局变量myname,但是,实际上并不是这么工作的。第一个alert会弹 出”undefined”是因为myname被当做了函数的局部变量(尽管是之后声明的),所有的变量声明当被悬置到函数的顶部了。因此,为了避免这种混 乱,最好是预先声明你想使用的全部变量。 上面的代码片段执行的行为可能就像下面这样: 复制代码 代码如下: myname = "global"; // global variable function func() { var myname; // 等同于 -> var myname = undefined; alert(myname); // "undefined" myname = "local"; alert(myname); // "local"} func();
为了完整,我们再提一提执行层面的稍微复杂点的东西。代码处理分两个阶段,第一阶段是变量,函数声明,以及正常格式的参数创建,这是一个解析和进入上下文 的阶段。第二个阶段是代码执行,函数表达式和不合格的标识符(为声明的变量)被创建。但是,出于实用的目的,我们就采用了”hoisting”这个概念, 这种ECMAScript标准中并未定义,通常用来描述行为。 for循环(for Loops) 在for循环中,你可以循环取得数组或是数组类似对象的值,譬如arguments和HTMLCollection对象。通常的循环形式如下: 复制代码 代码如下: // 次佳的循环 for (var i = 0; i < myarray.length; i++) { // 使用myarray[i]做点什么 }
集合的麻烦在于它们实时查询基本文档(HTML页面)。这意味着每次你访问任何集合的长度,你要实时查询DOM,而DOM操作一般都是比较昂贵的。 这就是为什么当你循环获取值时,缓存数组(或集合)的长度是比较好的形式,正如下面代码显示的: 复制代码 代码如下: for (var i = 0, max = myarray.length; i < max; i++) { // 使用myarray[i]做点什么 }
这样,在这个循环过程中,你只检索了一次长度值。 在所有浏览器下,循环获取内容时缓存HTMLCollections的长度是更快的,2倍(Safari3)到190倍(IE7)之间。//zxx:此数据貌似很老,仅供参考 注意到,当你明确想要修改循环中的集合的时候(例如,添加更多的DOM元素),你可能更喜欢长度更新而不是常量。 伴随着单var形式,你可以把变量从循环中提出来,就像下面这样: 复制代码 代码如下: function looper() { var i = 0, max, myarray = []; // ... for (i = 0, max = myarray.length; i < max; i++) { // 使用myarray[i]做点什么 } }
这种形式具有一致性的好处,因为你坚持了单一var形式。不足在于当重构代码的时候,复制和粘贴整个循环有点困难。例如,你从一个函数复制了一个循环到另一个函数,你不得不去确定你能够把i和max引入新的函数(如果在这里没有用的话,很有可能你要从原函数中把它们删掉)。 最后一个需要对循环进行调整的是使用下面表达式之一来替换i++。 复制代码 代码如下: i = i + 1 i += 1
JSLint提示您这样做,原因是++和–-促进了“过分棘手(excessive trickiness)”。//zxx:这里比较难翻译,我想本意应该是让代码变得更加的棘手 如果你直接无视它,JSLint的plusplus选项会是false(默认是default)。 还有两种变化的形式,其又有了些微改进,因为: 少了一个变量(无max) 向下数到0,通常更快,因为和0做比较要比和数组长度或是其他不是0的东西作比较更有效率 复制代码 代码如下: //第一种变化的形式: var i, myarray = []; for (i = myarray.length; i–-;) { // 使用myarray[i]做点什么 } //第二种使用while循环: var myarray = [], i = myarray.length; while (i–-) { // 使用myarray[i]做点什么 }
这些小的改进只体现在性能上,此外JSLint会对使用i–-加以抱怨。 for-in循环(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]); } }
严格来说,不使用hasOwnProperty()并不是一个错误。根据任务以及你对代码的自信程度,你可以跳过它以提高些许的循环速度。但是当你对当前对象内容(和其原型链)不确定的时候,添加hasOwnProperty()更加保险些。 格式化的变化(通不过JSLint)会直接忽略掉花括号,把if语句放到同一行上。其优点在于循环语句读起来就像一个完整的想法(每个元素都有一个自己的属性”X”,使用”X”干点什么): 复制代码 代码如下: // 警告: 通不过JSLint检测 var i, hasOwn = Object.prototype.hasOwnProperty; for (i in man) if (hasOwn.call(man, i)) { // 过滤 console.log(i, ":", man[i]); }
switch模式(switch Pattern) 你可以通过类似下面形式的switch语句增强可读性和健壮性: 复制代码 代码如下: var inspect_me = 0, result = ""; switch (inspect_me) { case 0: result = "zero"; break; case 1: result = "one"; break; default: result = "unknown"; }
这些通常快于parseInt(),因为parseInt()方法,顾名思意,不是简单地解析与转换。但是,如果你想输入例如“08 hello”,parseInt()将返回数字,而其它以NaN告终。 编码规范(Coding Conventions) 建立和遵循编码规范是很重要的,这让你的代码保持一致性,可预测,更易于阅读和理解。一个新的开发者加入这个团队可以通读规范,理解其它团队成员书写的代码,更快上手干活。 许多激烈的争论发生会议上或是邮件列表上,问题往往针对某些代码规范的特定方面(例如代码缩进,是Tab制表符键还是space空格键)。如果你是 你组织中建议采用规范的,准备好面对各种反对的或是听起来不同但很强烈的观点。要记住,建立和坚定不移地遵循规范要比纠结于规范的细节重要的多。 缩进(Indentation) 代码没有缩进基本上就不能读了。唯一糟糕的事情就是不一致的缩进,因为它看上去像是遵循了规范,但是可能一路上伴随着混乱和惊奇。重要的是规范地使用缩进。 一些开发人员更喜欢用tab制表符缩进,因为任何人都可以调整他们的编辑器以自己喜欢的空格数来显示Tab。有些人喜欢空格——通常四个,这都无所谓,只要团队每个人都遵循同一个规范就好了。这本书,例如,使用四个空格缩进,这也是JSLint中默认的缩进。 什么应该缩进呢?规则很简单——花括号里面的东西。这就意味着函数体,循环 (do, while, for, for-in),if,switch,以及对象字面量中的对象属性。下面的代码就是使用缩进的示例: 复制代码 代码如下: function outer(a, b) { var c = 1, d = 2, inner; if (a > b) { inner = function () { return { r: c - d }; }; } else { inner = function () { return { r: c + d }; }; } return inner; }
花括号{}(Curly Braces) 花括号(亦称大括号,下同)应总被使用,即使在它们为可选的时候。技术上将,在in或是for中如果语句仅一条,花括号是不需要的,但是你还是应该总是使用它们,这会让代码更有持续性和易于更新。 想象下你有一个只有一条语句的for循环,你可以忽略花括号,而没有解析的错误。 复制代码 代码如下: // 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i);
但是,如果,后来,主体循环部分又增加了行代码? 复制代码 代码如下: // 糟糕的实例 for (var i = 0; i < 10; i += 1) alert(i); alert(i + " is " + (i % 2 ? "odd" : "even"));
第二个alert已经在循环之外,缩进可能欺骗了你。为了长远打算,最好总是使用花括号,即时值一行代码: 复制代码 代码如下: // 好的实例 for (var i = 0; i < 10; i += 1) { alert(i); }