Javascript模式阅读笔记 · 基本技巧(一)

本章讨论一些写出高质量JavaScript代码的最核心的方法、模式和习惯,比如避免全局变量、使用单个var的声明、循环中重新缓存长度变量length(pre-caching length in loops)和符合代码约定等。

 

编写可维护的代码

软件bug的修改是需要成本的,并且这项成本总是在不断地增加,特别是对于已经广泛发布的产品代码而言,更是如此。最好的情况是当我们一发现bug,理科就可以修改它,这种情况只发生在刚写完这些代码不久。否则,一旦转移到新任务上,忘记了这部分代码,就需要重新阅读这些代码:

1. 花时间重新学习和理解相应的问题。

2. 花时间理解当时用于解决相应问题的代码。

对于大型项目或大公司而言,还存在另一个问题,就是最终修改代码的人,往往并不是当初写代码的人,也不是发现bug的人。因此,减少理解自己以前写的代码的时间,或者减少理解团队中他人写的代码的时间,就变得非常关键。同时,这也影响到开发完成时间(商业收入)和开发者的情绪,毕竟开发新产品更能让人兴奋,而不是花费那么多时间在老项目维护上。

另外一个事实在于,软件开发人员通常度代码比写代码更耗时间。通常的情形是,当我们专注于某个问题时,会坐下来花一下午时间编写出大量的代码。这些代码可能当天就可运行,但要想成为一项成熟的应用项目,需要我们对代码进行重新检查、重新校正、重新调整。譬如:

1. 发现bug。

2. 项目加入新的特性。

3. 项目需要在新的环境中运行。

4. 代码变换意图。

5. 代码在心的框架或体系下需要完全重写,甚至是应用一种新的语言。

因为这些改变,可能最初只是几小时工时写出来的代码,最终需要花费几周的工时来阅读。这就是为什么创建易维护的代码是一个项目成功与否的关键。

易维护的代码意味着代码具有如下特性:

1. 阅读性好。

2. 具有一致性。

3. 预见性好。

4. 看起来如同一个人编写。

5. 有文档。

本章接下来将阐述在开始写JavaScript脚本时要注意的关键。

 

尽量少用全局变量

JavaScript使用函数管理作用域。变量在函数内声明,只在函数内有效,不能在外部使用。全局变量与之相反,在函数外部声明,在函数内无需声明即可简单地使用。

每一个JavaScript环境都有全局对象,可在函数外部使用this进行访问。创建的每一个全局变量都为全局对象所有。在浏览器中,为了方便,使用window表示全局对象本身。下面的代码片段表明在浏览器中如何创建和访问全局变量。

myGlobal = "hello";
console.log(myGlobal);  // "hello"
console.log(window.myGlobal);  // "hello"
console.log(window["myGlobal"];  // "hello"
console.log(this.myGlobal);  // "hello"

 

全局变量的问题

全局变量的问题在于它们在整个JavaScript应用或Web页面内共享。它们生存与同一个全局命名空间内,总有可能发生命名冲突。譬如当一个应用程序中两个独立的部分定义了同名的全局变量,但却又不同的目的时。

网页经常会包含一些非页面开发人员编写的代码,譬如:

1. 第三方JavaScript库。

2. 来自于广告合作伙伴的脚本。

3. 来自于第三方用户的跟踪与分析脚本的代码。

4. 各种小工具、徽章(badges)和按钮。

举个例子,某个第三方脚本定义了一个全局变量result,后来又在某个函数里顶一个了另一个全局变量,也叫result。这样造成的结果是后一个result覆盖了前一个,第三方脚本可能就停止了工作。

因此,与同一个页面上的其他脚本有好共存非常重要,要尽可能少地使用全局变量。

JavaScript总是在不知不觉中就出人意料地创建了全局变量,其原因在于JavaScript的两个特性。第一个特性是JavaScript可直接使用变量,甚至无需声明。第二个特性是JavaScript有个暗示全局变量(implied globals)的概念,即任何变量,如果未经声明,就为全局对象所有(也就像正确声明过的全局变量一样可以访问)。

function sum(x, y) { // 反模式
    result = x + y; 
    return result; 
}

console.log(sum(3, 5));  // return => 8
console.log(result);  // return  => 8

上边的例子中,result未经声明就使用了。代码虽然在一般情况下可以正常工作,但如果在调用该函数后,在全局命名空间里使用了另外的result变量,问题就会出现。

function sum(x, y) {
    var result = x + y; 
    return result; 
}

console.log(sum(3, 5));  // return => 8
console.log(result);  // return  => throw error for result is not defined

另外一种创建隐式全局变量的反模式是带有var声明的链式赋值。

function Fn(){
    var a = b = 0;
    //...
}

通过运行以上代码,b成为全局变量,而我们的意愿可能是a与b都为局部变量。

出现此问题的原因在于从右至左的操作符优先级。以上代码的运行引申为:var a = ( b = 0 );

如果对链式赋值的所有变量都进行了声明,就不会创建出不期望的全局变量。

function Fn(){
    var a, b;
    a = b = 0;
}

注意:另一个避免全局变量的原因来源于代码移植,如果你希望你的代码运行在不同的环境(主机),使用全局变量就会非常危险。因为可能很偶然地,不存在于原环境(所以看起来是很安全的),但存在于其他环境的主机变量就被覆盖了。

 

变量释放时的副作用

隐含全局变量雨明确定义的全局变量有细微的不同,不同之处在于是否能使用delete操作符撤销变量。

1. 使用var创建的全局变量(这类变量在函数外部创建)不能删除。

2. 不使用var创建的隐含全局变量(尽管它是在函数内部创建)可以删除。

这表明隐含全局变量严格来讲不是真正的变量,而是全局对象的属性。属性可以通过delete操作符删除,但变量不可以。

var globalVar = 1;
globalNoVar = 2;
(function (){
    globalFormFn = 3;
});

delete globalVar;       // false
delete globalNoVar;     // true
delete globalFromFn;    // true

typeof globalVar;       // "number"
typeof globalNoVar;     // "undefined"
typeof globalFromFn;    // "undefined"

在ES5 strict模式中,为没有声明的变量赋值会抛出错误。

 

访问全局对象

在浏览器下,可通过window属性在代码的任意位置访问到全局对象(除非做了特别的处理而发生了意外,如声明了一个名为window的局部变量)。但在其他环境下,这个用起来方便的属性可能就不叫window,二十叫别的名称(甚至可能对于程序员是不可见的)。如果需要访问不带硬编码处理的标识window,可以按如下方式,从内嵌函数的作用域访问。

var global = (function (){
    return this;
})();

按这种方式通常能获得全局对象,因为this在函数内部作为一个函数调用(而不是通过构造器new新建)时,往往指向该全局对象。事实上在ECMAScript 5的严格模式下就不再这样用了。所以如果你的代码要运行在严格模式下,就要采用另一种方式。比如,如果你正在开发一个库,你可以将你的库里的代码打包在一个直接函数中,然后在全局作用域中,传递一个引用给this,把this看成传递到直接函数的一个参数。

 

单一var模式 (Single var Pattern)

只使用一个var在函数顶部进行变量声明市一中非常有用的模式。它的好处在于:

1. 提供一个单一的地址以查找到函数需要的所有局部变量。

2. 放置出现变量在定以前就被使用的逻辑错误。

3. 帮助牢记要声明变量,以尽可能少地使用全局变量。

4. 更少的编码(无论是输入代码还是传输代码都更少了)。

function Fn(){
    var a = 1,
        b = 2,
        sum = a + b,
        myObject = {},
        i, j;

     // 函数其他程序...
}

使用一个var关键字声明由逗号分隔的多个变量。在声明变量的同时初始化变量,为变量赋初始值也是一种好的做法。这样可以防止逻辑错误(所有为初始化且未声明的变量,其值都为undefined),也可提高代码的可读性。当你在以后重新看这段代码时,你可以根据变量的初始值知道使用这些变量的意图。比如,它应该是一个对象还是一个整型。

在声明变量时也可能做些实质性工作,比如上述代码的sum = a + b。另一个例子是DOM(文档对象模型)的引用。如下所示:

function updateElement(){
    var el = document.getElementById("result"),
        style = el.style;

    // 函数其他程序...
}

 

提升:零散变量的问题

JavaScript允许在函数的任意地方声明多个变量,无论是在哪里声明,效果都等同于在函数顶部进行声明。这就是所谓的“提升”。当先使用变量再在函数后面声明变量时可能会导致逻辑错误。对JavaScript而言,只要变量是在同一个范围(同一个函数)里,就视为已经声明,哪怕是在变量声明前就使用。

myname = "Peak";
function AlertName(){
    alert(myname);  // alert => undefined;
    var myname = "Peak Too";
    alert(myname);  // alert => "Peak Too"
}
AlertName();

在这个例子中,可能会以为第一个alert()会提示为“Peak”,第二个会提示为“Peak Too”,这是一个合乎情理的期望,因为在第一个alert中,myname没有声明,因此函数很有可能“看到”全局变量的myname。但事实并非如此,第一个alert会被指明为“undefined”,因为myname被看作声明为函数的本地变量(尽管是在后面声明)。所有的变量声明都提升到函数的最顶层。因此,为了避免混乱,最好在开始就声明要使用的所有变量。

注意:为了完整起见,再谈谈实现级别上的事情,事实上它们更为复杂。代码处理上分两个阶段,第一个阶段,这是创建变量、函数声明及形式参数,这是解析和进入上下文的阶段。第二个阶段是代码执行的过程,创建函数表达和不合格标识符(未定义变量)。但为了实际使用的目的,我们采用了“提升”的概念,这个概念并没有在ECMAScript标准中定义,但却经常用来表述这种情形。

 

for循环

for循环经常用在遍历数组或类数组对象,如引数(arguments)和HTML容器(HTMLColltion)对象。通常for循环模式使用如下:

for (var i = 0; i < myarray.length; i++){
    // 对myarray[i]操作...
}

这种模式的问题在于每次循环迭代时都要访问数据的长度。这样会使代码变慢,特别是当myarray不是数据,而是HTML容器对象时。

HTML容器是DOM方法返回的对象,如:

document.getElementsByName()

document.getElementsByClassName()

document.getElementsByTagName()

还有很多其他HTML容器,它们在DOM标准前就引入了,并一直使用至今。包括(除此之外还有很多):

document.images

document.links

document.forms

document.forms[0].elements

容器的麻烦在于它们在document(HTML页面)下是活动的查询。也就是说,每次访问任何容器长度时,也就是在查询活动的DOM,而通常DOM操作也是非常耗时的。

这就是为什么好的for循环模式是将已经便利过的数组(或容器)的长度缓存起来。如下所示:

for (var i = 0, imax = myarray.length; i < imax; i++){
    // 对myarray[i]操作...
}

这种方式下,对长度的值只提取一次,但应用到整个循环中。

在所有浏览器中,通过将HTML容器上需要遍历的次数缓存起来都会大大提高速度。其中在Safari 3中会提速两倍,而在IE7中速度会提高170倍。

注意,当要在循环中修改容器时,需要修改容器的长度。

下面是单变量模式,也可以将变量放到循环以外,如下所示:

function looper(){
    var i = 0,
        imax,
        myarray = [];

    //...

    for (i = 0, imax = myarray.length; i < imax; i++){
        // 对myarray[i]操作...
    }
}

这种模式的好处在于一致性,因为它贯穿了单一变量的模式。缺陷在于创建代码时粘贴和复制整个循环比较麻烦。例如,如果要从一个函数复制循环至另一个函数,必须确保能将i和imax携带至新函数中(如果这几个量在原函数中不再需要,则很可能会删除掉它们)。

对于循环的最后一个改进是,用i++替代i=i+1或i+=1

JSLint推荐这样做,原因是++和--提倡“excessive trickiness(过分棘手)”。

for模式中的两个变量引出了一些细微操作,原因是:

1. 使用了最少的变量(而非最多)。

2. 逐步减至0,这样通常更快,因为同0比较比同数组的长度比较,或同非0数组比较更有效率。

第一个修改后的模式是:

var i, myarray = [1, 2, 3, 4, 5];
for (i = myarray.length; i--;){
    // 对myarray[i]操作...
}

第二个使用while循环:

var myarray = [1, 2, 3, 4, 5], i = myarray.length;
while(i--){
    // 对myarray[i]操作...
}

 

for - in循环

for - in循环应该用来遍历非数组对象.使用for - in循环也被称为枚举(enumeration)。

从技术上说,也可以使用for - in来遍历数组(因为在JavaScript中,数组也是对象),但不推荐用户这样使用,因为当该数组已经被自定义函数扩大后,这样做有可能会导致逻辑上的错误。因此推荐使用正常的for循环来处理数组,并使用for - in循环来处理对象。

当遍历对象属性来过滤遇到原型链的属性时,使用hasOwnProperty()方法是非常重要的。

hasOwnProperty():是用来判断一个对象是否有你给出名称的属性或对象。不过需要注意的是,此方法无法检查该对象的原型链中是否具有该属性,该属性必须是对象本身的一个成员。

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

//将一个方法添加到所有对象上
if (typeof Object.prototype.clone === "undefined"){
    Object.prototype.clone = function (){};
}

在本例子中,使用文本定义了一个简单的名为man的对象。在man对象定义前面或者后面的其他位置,使用了一个名为clone()的有用的方法来增加Object的原型。该原型链是活动的,这也就意味着所有的对象都会自动获取针对新方法的访问。为了避免在枚举man的方法时举出clone()方法,需要调用hasOwnProperty()函数来过滤掉该原型属性。如果不使用过滤函数进行过滤,将会显示出clone(),这在大多数情形下是不希望得到的结果。

for (var i in man){
    if (man.hasOwnProperty(i)){
        console.log(i, ":", man[i]);
    }  
}

// 控制台输出
// hands : 2
// legs : 2
// heads : 1

如果未经过过滤,会得到以下结果:

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对man对象进行精炼后,可以有效地避免命名冲突,也可以使用一个本地变量来缓存比较长的属性名,如下所示:

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

注意:严格来讲,不使用hasOwnProperty()并没有错。依赖于具体任务和对代码的自信,您可以略过该方法并稍微加速循环执行的速度。但是当确认不了对象的内容(和原型链)时,最好还是通过此判断来区分。

 

不要增加内置的原型

增加构造函数的原型属性是一个增加功能性的强大的方法,但有时候该方法会过于强大。

增加内置构造函数(例如Object(),Array()和Function()等)的原型是很有诱惑的,但是这可能会严重影响可维护性,因为这种做法将使代码变得更加不可预测。其他开发者在使用您的代码时可能期望内置的JavaScript方法使用是一致的,而不期望有一些您自己添加的方法。

此外,给原型添加的属性在没有使用hasOwnProperty()时可能会在循环中出现,这会导致一些混乱。

因此,最好的方法就是不要给内置的原型增加属性。以下情形是例外,可以为内置原型增加属性:

1. 当未来的EMCAScript版本或JavaScript的具体实现可能将该功能作为一个统一的内置方法时。

2. 如果检查了自定义属性或方法并未存在时。也许在其他地方已经实现了该方法,或者是某个您支持的浏览器中JavaScript引擎的一部分。

3. 您准确地用文档记录下来,并和团队交流清楚。

如果遇到以上情形,可以采用如下模式为原型增加自定义方法:

if (typeof Object.prototype.myMethod !== "function"){
    Object.prototype.myMethod = function(){
        // 方法实现...
    }
}

 

switch模式

可以使用一下模式来提高switch语句的可读性和健壮性:

var inspectMe = 0, result = '';
switch (inspectMe){
    case 0:
        result = "zero";
        break;
    case 1:
        result = "one";
        break;
    default:
        result = "unknow";
}

switch语句通常采用如下格式:

1. 使每个case和switch纵向排列整齐(大括号的缩进规则是个例外)。

2. 在每个case语句中使用代码缩进。

3. 在每个case语句结尾有一个明确的break语句。

4. 避免使用fall-throughs(也就是有意不使用break语句,以使得程序会按顺序一直向下执行)。如果确实希望采用fall-throughs,那么请确信在代码中使用fall-throughs的确是最好的途径,因为在代码中这样做会让其他阅读您代码的人以为代码是有错误的。

5. 用default语句作为switch的结束:当以上情形不匹配时,给出一个默认的结果。

 

避免使用隐式转换

JavaScript在使用比较语句时会执行隐式类型转换,这也是为什么执行false==0或""==0这一类比较语句会返回true。

为了避免隐式类型转换导致的混淆不清,请在使用比较语句的时候使用===和!==操作符来对数值和类型进行比较:

var zero = 0;
if (zero === false){
    // 这里不会执行
}
if (zero == false){
    // 这里会执行
}

还有一种观念认为使用===是多余的,仅仅使用==就足够了。例如,当使用了typeof来获取已知类型时,会返回一个字符串,因此没有理由使用严格的等价比较。(JsLint要求严格的等价比较,这样的做法会使代码看起来更为一致,并减少在阅读代码时的脑力开销)

 

避免使用eval()

JavaScript有句俗语:“eval()是魔鬼”。该函数可以将任意字符串当作一个JavaScript代码来执行。当需要讨论的代码是预先就变写好了(不是在动态运行时决定),是没有理由 需要使用eval()。而如果代码是在运行时动态生成的,则也有其他更好的方法来代替eval()实现的功能,举例来说,只需要简单地使用方括号将需要访问的动态属性括起来就行了。

// 不推荐方法
var property = "name";
alert(eval("obj." + property));

// 推荐方法
alert(obj[property]);

使用eval()也包含一些安全隐患,因为这样做有可能执行被篡改过的代码(例如来自网络的代码)。这是在处理来自一个Ajax请求的JSON响应时常见的反模式。在那些情形下,最好是使用浏览器内置的方法来解析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);

使用new Function()构造函数和eval()比较类似,因此该函数的使用也需要十分小心,该函数是一个功能强大的函数,但通常容易被误用。如果一定需要使用eval(),那么可以考虑使用new Function()来代替eval()。这样做的一个潜在好处是由于在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);               // log => 1
jsstring = "var deux = 2; console.log(deux);";
new Function(jsstring)();     // log => 2
jsstring = "var trois = 3; console.log(trois);";
(function(){ eval(jsstring); })();  //log => 3

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

另一个new Function()和eval()的区别在于eval()会影响作用域链,而Funciton更多地类似于一个沙盒。无论是哪里执行Function,它都仅仅能看到全局作用域。因此对局部变量影响比较小。如下所示:

(function (){
    var local = 1;
    eval("local = 3; console.log(local);");  // log => 3
    console.log(local);  // log => 3
})();

(function (){
    var local = 1;
    Function("console.log(typeof local);")();  // log => undefined
})();

 

posted on 2013-09-30 16:04  雨梦萱橪  阅读(179)  评论(0编辑  收藏  举报