《编写可维护的JavaScript》读书笔记之编程实践-避免使用全局变量

避免使用全局变量

在浏览器中,window 对象往往重载并等同于全局对象,因此任何在全局作用域中声明的变量和函数都是 window 对象的属性。

【示例】:

var color = "red";

function sayColor() {
    alert(color);
}

console.log(window.color); // red
console.log(typeof window.sayColor); // function

【说明】:在这段代码中定义了全局变量 color 和全局函数 sayColor(),两者都是 window 对象的属性,尽管我们并没有显式地执行给 window 对象挂载属性的操作。

全局变量带来的问题

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

命名冲突

当脚本中的全局变量和全局函数越来越多时,发生命名冲突的概率也随之增高,即很可能无意间就使用了一个已经声明了的变量。

【好的实践】:所有的变量都被定义为局部变量,这样的代码才是最容易维护的。

【问题 1】:全局变量定义在外部文件。

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

【说明】:sayColor() 函数依赖于全局变量 color,而此时 color 定义在外部文件中,导致该依赖关系难以追踪。

【问题 2】:变量覆盖。

// 外部文件定义变量 color = "red";
// 当前 js 文件
color = "blue";
function sayColor() {
    alert(color); // "blue"
}

【说明】:color 在脚本中的定义存在多处,随着 sayColor() 函数被包含于代码的位置不同,其执行结果也不同,且难以定位到问题的所在。

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

代码的脆弱性

一个依赖于全局变量的函数即是深耦合于上下文环境之中。如果环境发生改变,函数很可能就失效了。这意味着任何对全局环境的修改都可能造成某处环境出错。同样,任何函数也会不经意间修改全局变量,导致对全局变量值的依赖变得不稳定。

【改进】:将全局变量当作参数传入到函数中。

function sayColor(color) {
    alert(color);
}

【说明】:修改后的函数不再依赖于全局变量,因此任何对全局环境的修改都不会影响到它。

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

难以测试

任何依赖全局变量才能正常工作的函数,只有为其重新创建完整的全局环境才能正确地测试它。事实上,这意味着你除了要管理全局环境的修改,还要在两个全局环境中管理它们:生产环境和测试环境。保持两者的同步是很消耗成本的。

【建议】:确保你的函数不会对全局变量有依赖,这将增强你的代码的可测试性(testability)。

【注意】:对于原生的 JavaScript 全局对象,比如 Date、Array 等。它们是全局环境的一部分,是和 JavaScript 引擎相关的。有的时候,你的函数总是会依赖这些全局对象,这是没有问题的。需要注意的是避免对开发者自行创建的全局变量的依赖。

意外的全局变量

当你给一个未被 var 语句声明过的变量赋值时,JavaScript 会自动创建一个全局变量。

【示例】:

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

【错误 1】:不小心引入了一个全局变量:作者原本的意图是用单 var 语句声明两个变量,但在第一个变量后不小心敲入了分号而不是逗号,结果就将 title 创建成了全局变量。

【错误 2】:不小心省略 var 语句可能意味着在不知情的情况下修改某个已存在的全局变量,甚至是 window 已有的属性。

【好的实践】:总是使用 var 来定义变量,哪怕是定义全局变量。这样做能大大降低某些场景里省略 var 所导致错误的可能性。

避免意外的全局变量

不小心创建全局变量时,JavaScript 不会报任何警告。这时就需要一些其他工具和方式来发挥作用。

  • JSLint、JSHint:如果你给一个未声明的变量赋值,这两个工具都会报警告。不小心修改全局变量的值或试图给其他全局对象赋值,也会给出提示。
  • use strict:严格模式下,解析和执行 JavaScript 的规则发生了变化。其中一个规则的变化就是严格模式可以探测未声明变量的赋值操作。

【注意】:

  1. 不是所有的浏览器环境都支持严格模式(IE 10+、FireFox 4+、Safari 5.1+、Opera 12+ 或 Chrome)。
  2. 如果你在处理老的代码时,则要非常小心地使用严格模式。对于新的代码,最好总是使用严格模式来避免意外的全局变量。同时其他一些常见的编程错误也能在严格模式下被捕捉到。

单全局变量方式

“单全局变量”的意思是所创建的这个唯一全局对象名是独一无二的(不会和内置 API 产生冲突),并将所有的功能代码都挂载到这个全局对象上。因此每个可能的全局变量都成为唯一全局对象的属性,从而不会创建多个全局变量。

【示例】:

function Book(title) {
    this.title = title;
    this.page = 1;
}

Book.prototype.turnPage = function (direction) {
    this.page += direction;
};

var Chapter1 = new Book("Introduction to Style Guidelines");
var Chapter2 = new Book("Basic Formatting");
var Chapter3 = new Book("Comments");

// 单全局变量
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("Introduction to Style Guidelines");
MaintainableJS.Chapter2 = new MaintainableJS.Book("Basic Formatting");
MaintainableJS.Chapter3 = new MaintainableJS.Book("Comments");

命名空间

即使代码中只有一个全局对象,也存在着全局污染的可能性。大多数使用单全局变量模式的项目同样包含“命名空间”的概念。

【概念】:通过全局对象的单一属性表示的功能性分组。

【举例】: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 += 1) {
            if (!object[parts[i]]) {
                object[parts[i]] = {};
            }
            object = object[parts[i]];
        }
        
        return object;
    }
};


基于单全局对象使用 namespace() 方法可以让开发者放心地认为命名空间总是存在的。每个文件都可以首先调用 namespace() 来声明开发者将要使用的命名空间,这么做不会对已有的命名空间造成任何破坏。可以让开发者得到解放,在使用命名空间之前不必再去判断它是否存在。

模块

另外一种基于单全局变量的扩充方法是使用模块(modules)。模块是一种通用的功能片段,并没有创建新的全局变量或命名空间。相反,所有的代码都存放于一个表示执行一个任务或发布一个接口的单函数中。

零全局变量

JavaScript 代码注入到页面时可以做到不用创建全局变量。

【常见的情形】:一段不会被其他脚本访问到的完全独立的脚本。

【实现方法】:使用立即执行的函数调用,并将所有脚本放置其中。

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

【限制】:

  • 只要代码需要被其他代码所依赖,就不能使用零全局变量的方式。
  • 代码需要运行时被不断扩展或修改也不能使用零全局变量的方式。

你可能感兴趣的:(《编写可维护的JavaScript》读书笔记之编程实践-避免使用全局变量)