前面的模式通过自定义类型创建了私有变量和特权方法。而下面要讨论的 Douglas Crockford 所说的模块模式,则在一个单例对象上实现了相同的隔离和封装。单例对象(singleton)就是只有一个实例的对象。按照惯例,JavaScript 是通过对象字面量来创建单例对象的,如下面的例子所示:
name: value,
method() {
// 方法的代码
}
};
``
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。模块模式的样板代码如下:
```let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}();
模块模式使用了匿名函数返回一个对象。在匿名函数内部,首先定义私有变量和私有函数。之后,创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因为这个对象定义在匿名函数内部,所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。本质上,对象字面量定义了单例对象的公共接口。如果单例对象需要进行某种初始化,并且需要访问私有变量时,那就可以采用这个模式:
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 公共接口
return {
getComponentCount() {
return components.length;
},
registerComponent(component) {
if (typeof component == 'object') {
components.push(component);
}
}
};
}();
在 Web 开发中,经常需要使用单例对象管理应用程序级的信息。上面这个简单的例子创建了一个application 对象用于管理组件。在创建这个对象之后,内部就会创建一个私有的数组 components,然后将一个 BaseComponent 组件的新实例添加到数组中。(BaseComponent 组件的代码并不重要,在这里用它只是为了说明模块模式的用法。)对象字面量中定义的 getComponentCount()和 registerComponent()方法都是可以访问 components 私有数组的特权方法。前一个方法返回注册组件的数量,后一个方法负责注册新组件。 在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是 Object 的实例,因为最终单例都由一个对象字面量来表示。不过这无关紧要,因为单例对象通常是可以全局访问的,而不是作为参数传给函数的,所以可以避免使用 instanceof 操作符确定参数是不是对象类型的需求。 模块增强模式 另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。来看下面的例子:
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
let object = new CustomType();
// 添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
// 返回对象
return object;
}();
如果前一节的 application 对象必须是 BaseComponent 的实例,那么就可以使用下面的代码来创建它:
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 创建局部变量保存实例
let app = new BaseComponent();
// 公共接口
app.getComponentCount = function() {
return components.length;
};
app.registerComponent = function(component) {
if (typeof component == "object") {
components.push(component);
}
};
// 返回实例
return app;
}();
在这个重写的 application 单例对象的例子中,首先定义了私有变量和私有函数,跟之前例子中一样。主要区别在于这里创建了一个名为 app 的变量,其中保存了 BaseComponent 组件的实例。这是最终要变成 application 的那个对象的局部版本。在给这个局部变量 app 添加了能够访问私有变量的公共方法之后,匿名函数返回了这个对象。然后,这个对象被赋值给 application。 小结 函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而让开发者可以更有效地使用函数。
有名称的函数表达式也被称为匿名函数。
ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。
JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符,
可以实现函数定义和调用的完全动态化。
函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了
什么参数等信息。
JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。
闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象。
通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。
闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。
立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都会被销毁。
虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域中定义的变量。
可以访问私有变量的公共方法叫作特权方法。
特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增
强模式在单例对象上实现。