十一

内存泄漏
由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁:

function assignHandler() {
    var element = document.getElementById("someElement");
    element.onclick = function () {
        alert(element.id);
    };
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决:

function assignHandler() {
    var element = document.getElementById("someElement");
    var id = element.id;
    element.onclick = function () {
        alert(id);
    };
    element = null;
}

在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够接触对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域
JavaScript没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的:

function outputNumbers(count) {
    for (var i = 0; i < count; i++) {
        alert(i);
    }
    alert(i); //计数
}

在JavaScript中,变量i是定义在ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即使像下面这样错误地重新声明同一个变量,也不会改变它的值:

function outputNumbers(count) {
    for (var i = 0; i < count; i++) {
        alert(i);
    }
    var i; //重新声明变量
    alert(i); //计数
}

JavaScript从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,它会执行后续声明中的变量初始化)。匿名函数可以用来模仿块作用域并避免这个问题。
用作块级作用域(通常称为私有作用域)的匿名函数的语法如下:

(function(){
    //这里是块级作用域
})();
function(){
  //从这里是块级作用域
}(); //出错!

上一段代码会导致语法错误,因为JavaScript将function关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。然而,函数表达式的后面可以跟圆括号。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域:

function outputNumbers(count) {
    (function () {
        for (var i = 0; i < count; i++) {
            alert(i);
        }
    })();
    alert(i); //导致一个错误!
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量
严格来讲,JavaScript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
私有变量在函数外部不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法:

function MyObject() {
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //特权方法
    this.publicMethod = function () {
        privateVariable++;
        return privateFunction();
    };
}

在构造函数中定义特权方法有一个缺点,那就是你必须使用函数模式来达到这个目的。前面已经讨论过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法可以避免这个问题。

静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法:

(function () {
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //构造函数
    MyObject = function () {
    };
    //公有/特权方法
    MyObject.prototype.publicMethod = function () {
        privateVariable++;
        return privateFunction();
    };
})();

需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明MyObject时使用var关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

(function () {
    var name = "";
    Person = function (value) {
        name = value;
    };
    Person.prototype.getName = function () {
        return name;
    };
    Person.prototype.setName = function (value) {
        name = value;
    };
})();
var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"

以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用闭包和私有变量的一个明显的不足之处。

模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式则是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。

var singleton = {
  name : value,
  method : function () {
    //这里是方法的代码
  }
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强:

var singleton = function () {
    //私有变量和私有函数
    var privateVariable = 10;
    function privateFunction() {
        return false;
    }
    //特权/公有方法和属性
    return {
        publicProperty: true,
        publicMethod: function () {
            privateVariable++;
            return privateFunction();
        }
    };
}();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的:
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是Object的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没有什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用instanceof操作符来检查其对象类型了。

增强的模块模式

var application = function () {
    //私有变量和函数
    var components = new Array();
    //初始化
    components.push(new BaseComponent());
    //公共
    return {
        getComponentCount: function () {
            return components.length;
        },
        registerComponent: function (component) {
            if (typeof component == "object") {
                components.push(component);
            }
        }
    };
}();

变为:

var application = function () {
    //私有变量和函数
    var components = new Array();
    //初始化
    components.push(new BaseComponent());
    //创建application的一个局部副本
    var 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对象添加了能够访问私有变量的公有方法。最后一步是返回app对象,结果仍然是将它赋值给全局变量application。

你可能感兴趣的:(十一)