第六章 零全局变量

  • A+
所属分类:Web前端
摘要

一般来讲,创建全局变量被认为是最糟糕的实践,尤其是在团队开发的大背景下更是问题多多。随着代码量的增长,全局变量会导致一些非常重要的可维护性难题。全局变量越多,引入错误的概率也就越来越高。


避免使用全局变量

一般来讲,创建全局变量被认为是最糟糕的实践,尤其是在团队开发的大背景下更是问题多多。随着代码量的增长,全局变量会导致一些非常重要的可维护性难题。全局变量越多,引入错误的概率也就越来越高。

命名冲突

当脚本中的全局变量和全局函数越来越多的时候,发生命名冲突的概率随之增高,即很可能无意间使用了一个已经声明的变量。多有的变量都被定义为局部变量,这样代码才是最容易维护的。

function sayColor() {     alert(color); // 不好的做法:color是哪来的? } 

让我们更进一步,如果color在脚本的定义存在很多处,那么随着sayColor()函数被包含于代码的位置不同,其执行结果也不尽相同。

全局环境是用来定义JavaScript内置对象的地方,如果你给这个作用域添加上了自己的变量,接下来则会面临读取浏览器附带的内置变量的风险。 比如,对于名字为color来说,绝对是一个不安全的全局变量。它只是一个普通名词,并没有任何的限定符,因此和浏览器未来的内置API或其他开发者的代码产生冲突的可能性极高。

代码的脆弱性

一个依赖于全局变量的函数即使深耦合于上下文环境中。如果环境发生改变,函数很可能失效。上一个例子,如果color不存在,sayColor()方法将会报错。意味着任何对全局环境修改都可能会造成某处代码的出错。同样,任何函数也会不经意间修改全局变量,导致对全局变量值得依赖变得不稳定。在上一个例子中,如果color当做参数传入,代码可维护性变得更佳。

function sayColor(color) {     alert(color); // 不好的做法:color是哪来的? } 

修改后的这个函数不再依赖全局变量,因此任何对全局环境的修改不会影响到它。color是一个参数,唯一值得注意的是传入的函数值是否合法

当定义函数的时候,最好尽可能多的将数据置于局部作用域中,在函数内定义的“任何东西”都应该采用这种写法,任何来自函数外部的数据都应当以参数形式传入进来。这样做可以将函数与外部环境隔开,并且你的修改不会对程序其他部分造成影响。

难以测试

确保你的函数不会对全局变量有依赖,这将增强你的代码的可测试性。当然,你的函数可能会依赖原生的JavaScript全局对象,比如Date,Array等。他们是全局环境的一环,是和JavaScript引擎有关的,你的函数总是会用带到这些全局对象。总之,为了保证你的代码具有最佳的可测试性,不要让函数对全局变量有依赖

可以依赖全局对象(Data,Array ...),但是尽量减少全局变量的依赖

意外的全局变量

JavaScript中有不少陷阱,其中一个就是不小心创建全局变量。当你给一个未被var语句声明的变量赋值的时候,JavaScript就会自动创建一个全局变量。比如:

function doSomething() {     var count = 10;     title = "test";   // 不好的写法,创建了全局变量 } 

不小心省略var语句可能意味着在你不知情的情况下修改了某个已存在的全局变量。

function doSomething() {     var count = 10;     name = "test";   // 不好的写法,创建了全局变量 } 

name实际上是window的一个默认属性。window.name属性经常用于框架(frame)和iframe的场景中,当点击链接时,可以通过指定打开链接的目标容器;来控制其在特定的框架或选项卡中显示,不小心修改name会影响到站点的链接导航。

最好的规则就是总是使用var定义变量,哪怕是定义全局变量,这样大大降低默写场景省略var所导致错误的可能性

避免意外的全局变量

使用严格模式,上面那种为使用var声明变量的方式会报错

function doSomething() {     var count = 10;     name = "test";   // 引用错误:foo未被定义 } 

单全局变量方式

单全局变量模式已经在各种流行的JavaScript类库中广泛使用了

  • YUI定义唯一一个YUI全局对象
  • jQuery定义了两个全局对象 $jQuery
  • Dojo定义一个dojo全局对象
  • Closure类库定义了一个goog全局对象

“单全局变量”的意思是所创建的这个唯一全局对象名是独一无二的(不会和内置API产生冲突),并将你所有功能代码都挂载到这个全局对象上

function Book(title) {     this.title = title;     this.page = 1; }  Book.prototype.turnPage = function(direction) {     this.page += direction; }  var Chapter1 = new Book("A"); var Chapter2 = new Book("B"); var Chapter3 = new Book("C"); 

这段代码创建了四个全局对象:Book,Chapter1,Chapter2,Chapter3。单全局变量模式则只会创建一个全局对象并将这些对象都复制为它的属性。

var MaintainableJS = {};  MaintainableJS.Book = function(title) {     this.title = title;     this.page = 1; }  MaintainableJS.Book.prototype.turnPage = function(direction) {     this.page += direction; }  MaintainableJS.Chapter1 = new MaintainableJS.Book("A"); MaintainableJS.Chapter2 = new MaintainableJS.Book("B"); MaintainableJS.Chapter3 = new MaintainableJS.Book("C"); 

这段代码只有一个全局对象,即MaintainableJS,其他任何信息都挂载在这个的对象上。因为团队都知道这个全局对象因此容易做到给其添加属性,以避免全局污染

命名空间

即使你的代码只有一个全局对象,也存在全局污染的可能性。大多数使用全局变量模式的项目同样包含“命名空间”的概念。命名空间是简单的对通过全局对象单一属性表示的功能性分组。比如,YUI就是依照命名空间的思路来管理代码的。Y.DOM下的所有方法都是和DOM操作相关的,Y.Event下的所有方法都是和事件相关的,以此类推。

var ZakasBooks = {};  // 表示这本书的命名空间 ZakasBooks.MaintainableJavaScript = {};  // 表示另一本书的命名空间 ZakasBooks.HighPerformanceJavaScript = {}; 

一个常见的约定是每个文件中都通过创建新的全局对象来声明自己的命名空间。在这种情况下,上面的案例是够用的。

同样有另外一些场景,,诶个文件都需要给命名空间挂载东西,这个时候你需要首先保证这个命名空间是已经存在的。这是全局对象非破坏性的处理命名空间的方式变得非常有用。

var YourGlobal = {     namespace : function(ns) {         var parts = ns.split("."),             object = this,             i, len;                  for (i = 0, len = parts.length; i < len; i++) {             if(!object[parts[i]]) {                 object[parts[i]] = {};             }             object = object[parts[i]];         }         return object;     } } 

变量YourGlobal实际上可以表示任意名字。最重要的部分在于namespace()方法,我们给这个方法传入一个表示命名空间对象的字符串,它非破坏性地创建一个命名空间。

/*  * 同时创建YourGlobal.Books和YourGlobal.Books.MaintainableJavaScript  * 因为之前没有创建他们,因此每个都是全新创建的  */ YourGlobal.namespace("Books.MaintainableJavaScript");  // 你现在可以使用该命名空间了 YourGlobal.Books.MaintainableJavaScript.author = "Nicholas C. Zakas";  /*  * 不会操作YourGlobal.Books本身,同时会给它添加HighPerformanceJavaScript  * 他会保持YourGlobal.Books.MaintainableJavaScript原封不动  */ YourGlobal.namespace("Books.HighPerformanceJavaScript");  // 仍然是合法的引用 console.log(YourGlobal.Books.MaintainableJavaScript.author)  // 你同样可以在方法调用之后立即给它添加新属性 YourGlobal.namespace("Books").ANewBook = {}; 

模块

另一种基于单全局变量的扩充方法是使用模块。模块是一种通用的功能片段,它并没有创建新的全局变量或命名空间。相反,所有的这些代码都存放于一个表示执行任务或发布一个接口的单函数中。可以用一个名称来表示一个模块,同样这个模块可以依赖其他模块

零全局变量

你的JavaScript代码注入到页面时是可以做到不用创建变量的。这种方法应用场景不多,因此只有在某些特殊场景下才会有用。最常见的情形就是一段不会被其他脚本访问到的完全独立的脚本。之所以存在这样的情形,是因为所有所需的脚本都会合并到一个文件,或者因为这段非常短小且不提供任何借口的代码会被插入至一个页面中。最常见的用法是创建一个书签

书签是独立的,他们并不知晓页面内容包含什么且不需要页面知道它的存在。最终我们需要一段“零全局变量”的脚本嵌入到页面中,实现方法就是使用一个立即执行的函数调用并将所有脚本放置其中,比如:

(function(win) {     var doc = win.document;          // 在这定义其他变量          // 其他相关代码      }(window)); 

这段立即执行的代码传入window对象,因此这段代码不需要直接引用任何全局变量。在这个函数内部,变量doc是指向document对象的引用,只要是函数代码中没有直接修改window或doc且所有变量都是用var声明,这段脚本则可以注入到页面中而不会产生任何全局变量。