编程实践 - 第 6 章 避免使用全局变量
《编写可维护的 JavaScript》—— Nicholas C. Zakas
JavaScript 执行环境在很多方面都有其独特之处。全局变量和函数的使用便是其中之一。
JavaScript 的初始执行环境是由多种多样的全局变量所定义的,这些全局变量在脚本环境创建之初就已经存在了。
我们说这些都是挂载在“全局对象”(global object)上的,“全局对象”是一个神秘的对象,它表示了脚本的最外层上下文。
在浏览器中,window
对象往往重载并代理了全局对象,因此任何在全局作用域中声明的变量和函数都是 window
对象的属性,比如:
var color = 'red';
function getColor() {
return color;
}
console.log(window.color);
consoel.log(window.getColor());
color
、getColor
都是 window
对象的属性,即使没有显式挂载。
1. 全局变量带来的问题
创建全局变量是糟糕的实践。全局变量越多,引入错误的概率将会因此变得越来越高。
1.1. 命名冲突
当脚本中的全局变量和全局函数越来越多时,发生命名冲突的概率也随之增高。
所有变量都被定义为局部变量,这样代码才是最容易维护的。
全局函数、全局变量如果分隔到不同的文件,其依赖关系很难追踪到:
function getColor() {
return color; // color 是哪里来的?
}
全局环境是用来定义 JavaScript 内置对象的地方,如果你在全局环境定义了变量,则很可能与浏览器附带的内置变量冲突。
1.2. 代码的脆弱性
一个依赖于全局变量的函数,与上下文环境深耦合。
如果环境发生改变,函数很可能就失效了,如果全局变量 color
不存在,getColor()
函数会报错。
这意味着,任何对全局环境的变量进修改都可能造成某处代码出错;
同样,任何函数也会不经意间修改全局变量,导致对全局变量值的依赖变得不稳定。
当定义函数的时候,最好尽可能多地将数据至于局部作用域。
任何来自函数外部的数据都应当以参数形式传进来,这样做可以将函数和外部环境隔离开来,并且在函数内的修改不会影响到程序的其他部分。
1.3. 难以测试
任何依赖全局变量才能正常工作的函数,只有为其重新创建完整的全局环境才能正确地测试它。
这意味着,你得同时管理测试、生产情况下的全局环境,一旦全局环境发生变化,则要进行同步。
这会增加维护的成本,且越来越难以理清头绪。
确保你的函数不会对全局变量有依赖,这会提高可测试性(testability)。
2. 意外的全局变量
当你给一个未被声明的变量赋值时,JavaScript 会自动创建一个全局变量,比如:
function doSomething() {
var count = 10;
title = '哇哈哈'; // 不好的写法:创建了全局变量。
}
上面的代码展示了一个常见的错误,不小心将 ,
敲成了 ;
,造成创建了一个全局变量 title
。
可使用 JSLint 或 JSHint 来给予警告。
也可启用严格模式,在支持严格模式的环境(IE 10+、Firefox 4+、Chrome)中,给未声明的变量赋值会抛出一个 ReferenceError 错误。
3. 单全局变量方式
“单全局变量”的意思是所创建的这个唯一全局对象是独一无二的,并将你所有的功能代码都挂载到这个全局对象上。
因此每个可能的全局变量都成为了你唯一全局对象属性,从而不会创建多个全局变量。
因为团队中的每个人都知道这个全局对象,因此很容易做到继续为它添加属性以避免全局污染。
- YUI 定义了
YUI
全局对象 - jQuery 定义了
$
和jQuery
- Dojo 定义了
dojo
- Closure 定义了
goog
3.1. 命名空间
即使你的代码只有一个全局对象,也存在着全局污染的可能。
大多数使用单全局变量模式的项目同样包含“命名空间”的概念。
命名空间是通过全局对象的属性将功能进行分组,比如,YUI
Y.DOM
下的所有方法都和 DOM 操作相关Y.Event
与事件相关
3.2. 模块
另外一种基于单全局变量的扩充方法是使用模块(modules)。
模块是一种通用的功能片段,它并没有创建新的全局变量或者命名空间;
模块的代码存在于一个函数中,用于执行任务或暴露接口;
可以用名称表示这个模块,同样这个模块可以依赖其他模块。
有一些通用的模式用来创建模块,最流行的是 “YUI 模式” 和异步模块定义(Asynchronous Module Definition,简称 AMD)模式。
YUI 模块:
// 定义模块
YUI.add('module-name', function(Y) {
Y.namespace('System.dept')
}, 'version', { requires: [ 'dep1', 'dep2' ] });
// 使用模块
YUI().use('module-name', function(Y) {
// Y.System.dept
});
将命名空间和模块的概念合并在一起了
异步模块定义(AMD):
指定模块名称、依赖、工厂方法,依赖加载完成后执行这个工厂方法。
这些内容都作为参数传递到 define()
全局函数:
define('module-name', [ 'dep1', 'dep2' ], function(dep1, dep2) {
var person = {
name: '张三',
say: function() {
console.log(this.name);
},
}
return person;
});
AMD 中每一个依赖都会对应到独立的参数传入工厂方法里,以避免命名冲突,并返回公有接口。
要想使用 AMD 模块,还需要一个与之兼容的模块加载器。
比较著名的加载器是 RequireJS,它添加了另一个全局函数 require()
,专门用来加载指定的依赖和执行回调函数,比如:
require([ 'module-name' ], function(person) {
person.say();
});
调用 require()
时会首先加载依赖,依赖加载并执行完成后执行回调函数。
4. 零全局变量
(function(win) {
var doc = win.document;
// 其他代码
}(window));
不需要暴露接口,也不需要依赖其他的脚步;作为完全独立的脚步插入到页面中。