JS学习笔记(四)(闭包)

最近学这块知识学得有些吃力。还有很多遗漏的地方,只能以后多看些书来弥补了。
<h1 id="7">第7章 函数表达式</h1>

函数定义的两种方式:函数声明,函数表达式。这个在第5章函数声明与函数表达式也有提到的
函数声明:

  • 语法:function functionName(){}
  • 一些浏览器给函数定义了一个非标准的name属性,这个属性值=functionName
  • 函数声明提升:执行代码之前会先读取函数函数声明。就是说可以把函数声明放在调用它的语句之后

函数表达式:

  • 函数表达式有几种不同的语法形式
  • 这个比较常见:var functionName = function(){};
  • 用以上创建的函数叫匿名函数(name属性是空字符串)。
  • 既然是表达式,在使用前就要先赋值

<h2 id="7.1">7.1 递归</h2>

arguments.callee这是个很有用的属性,指向正在执行的函数的指针。可以用它实现对函数的递归调用,第5章函数内部属性也讲到过。贴一段经典代码!

function factorial(num){
    if (num <= 1){
        return 1;
    }else{
    return num * arguments.callee(num-1);
    }
}

arguments.callee比用函数名更保险。不过严格模式下会有错误。

var factorial = (function f(num){
    if (num <= 1){
        return 1;
    }else{
    return num * arguments.callee(num-1);
    }
});

以上代码就是把函数f赋值给factorial了

<h2 id="7.2">7.2 闭包</h2>

这里刚开始看的时候有许多概念不懂,导致前后不能连贯,对作用域链一直存在疑问。所以我首先列举一下一些重要的概念。之前在第4章提到过的执行环境。我看了当时做的笔记,记的不是很完整。 所以在这里也补充一下

执行环境

  • 定义了变量或函数有权访问的其他数据

  • 每个执行环境都有一个与之关联的变量对象。这个变量对象是用来保存环境中定义的所有变量和函数的

  • 执行环境可以分为全局执行环境和函数执行环境

    • 全局执行环境直到应用程序退出时才会销毁
    • 每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。执行完毕后栈将其环境弹出
  • 当代码在一个环境中执行时,会创建变量对象的一个作用域链

作用域链:

  • 作用域链的前端始终都是当前执行代码所在环境的变量对象
  • 作用域链的下一个变量对象来租包含(外部)环境
  • 如果当前执行代码所在环境是函数,则将其活动对象作为变量对象
  • 活动对象最开始只包含一个变量,即arguments对象。

变量对象:

  • 保存了环境中定义的所有变量和函数
  • 每个执行环境都有一个变量对象
  • 局部环境的变量对象只在函数执行的过程中存在
  • 全局环境变量对象始终存在(这两点和执行环境相通)
  • 变量对象存储着环境中的以下内容
    • 函数的形参
    • var声明的变量
    • 函数声明(但不包含函数表达式)

活动对象:

  • 活动对象就是作用域链上正在被执行和引用的变量对象

当创建一个函数时:

  1. 创建预先包含全局变量对象的作用域链
  2. 这个作用域链保存在函数内部的[[Scope]]属性中

当第一次调用函数时:

  1. 创建一个执行环境(注意要调用的时候才会有执行环境)
  2. 复制函数的[[Scope]]属性中的对象,构建这个执行环境的作用域链
  3. 创建一个活动对象,使用this、arguments和其他命名参数的值来初始化函数的活动对象,把这个活动对象推入执行环境作用域链的前端
  4. 外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象在第三位,以此类推,直到作为作用域链终点的全局执行环境

下面结合书上的例子来看一下上述的两个过程

function compare(value1, value2){  
    if(value1 < value2){  
        return -1;  
    }else if(value1 > value2){  
        return 1;  
    }else{  
         return 0;  
    }  
}
var result = compare(5, 10);
  1. 创建这个函数的时候:


    创建.png
  2. 第一次调用
  • 复制作用域链到执行环境中


    这个时候还只有全局变量对象的作用域链.png
  • 把活动对象推入执行环境作用域链的前端


    加入了活动对象的作用域链.png

一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是闭包的情况要特殊一点。

在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。下面看另一个例子

function createComparisonFunction(propertyName){
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2){
            return -1;
        }else if (value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
}
//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({name:"xjh"},{name:"xkld"});
//解除对匿名函数的引用(以便释放内存)
compareNames = null;

也就是说这个时候里面的匿名函数是有权访问propertyName的

<h3 id="7.2.1">7.2.1 闭包与变量</h3>

闭包保存的是整个变量对象,不是某个特殊的值

<h3 id="7.2.2">7.2.2 关于this对象</h3>

this对象

  • 是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,当函数被作为某个对象的方法调用时,this等于那个对象。
  • 匿名函数的执行环境具有全局性,因此this对象通常指向window。

<h3 id="7.2.3">7.2.3 内存泄漏</h3>

如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素无法被销毁

<h2 id="7.3">7.3 模仿块级作用域</h2>

JS中没有块级作用域的概念,任何变量都是在函数中创建的
当重复声明一个变量时,只会对后续的声明视为不见(不过可以执行后续声明的变量初始化)

模拟块级作用域(私有作用域):

(function(){        //在外面加()的目的是把函数声明转换成函数表达式。JS中函数声明后面不能加圆括号
    //块级作用域
})();

这段代码定义并调用了一个匿名函数,将函数声明包含在()中,最后的()会立即调用这个函数
定义匿名函数可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链

<h2 id="7.4">7.4 私有变量</h2>

JS没有私有成员的概念:所有对象属性都是共有的,不过有一个私有变量的概念:不能在函数的外部访问这些变量
私有变量包括函数的参数、局部变量、在函数内部定义的其他函数
可以利用闭包创建用于访问私有变量的公有方法
特权方法:有权访问私有变量和私有函数的公有方法

创建特权方法的两种方式:

  1. 在构造函数中定义特权方法
    function MyObject(){
        var privateVar = 10;
        function privateFunction(){
            return false;
        }
        this.publicMethod = function(){
            privateVar++;
            return privateFunction();
        };
    }

这个privateVar和privateFunction可以看成是私有的,因为在别的地方无法访问到这些变量(可以参考私有作用域for循环里面的i)
publicMethod因为是对象的属性(前面有this的),这样的话用构造函数创建一个实例的时候就有了这个公有的方法。而这个publicMethod的作用域链包含着MyObject的作用域链,就可以访问到对应的私有变量了
看如下代码:

    function Person(name){
        this.getName = function(){
            return name;
        };
        this.setName = function(value){
            name = value
        };
    }

getName()和setName()作为闭包能通过作用域链访问name。私有变量name在Person的每一个实例中都不同,每次调用构造函数都会重新创建这两个方法。这个方法和通过构造函数创建对象一样有个缺点,就是每次都要重新创建get和set这两个方法

  1. 使用静态私有变量

<h3 id="7.4.1">7.4.1 静态私有变量</h3>

(function(){
    var privateVar = 10;
    function privateFunction(){
        return false;
    }
    MyObject = function(){//定义了一个全局变量MyObject指向这个匿名构造函数
    }; 
    MyObject.prototype.publicMethod = function(){
        ...
    };
})();

以上代码主要是在私有作用域里面定义了一个构造函数。利用这个构造函数的原型访问私有变量
4.2中提到过:使用var声明的变量会自动被添加到最接近的环境中。在函数内部就是局部环境,with语句中就是函数环境。不使用var声明变量自动添加到全局环境。这里定义构造函数时不用函数声明(function MyObject())的主要原因是函数声明只能创建局部函数。而用函数表达式,变量前不加var就可以定义一个全局变量,能够在私有作用域外被访问到(严格模式下会报错)

但是这个方法每个实例都可以对name进行修改,name就成为一个静态私有变量

<h3 id="7.4.2">7.4.2 模块模式</h3>

前面两种模式主要用于为自定义类型创建私有变量和特权方法。模块模式是为单例创建私有变量和方法
JS中以对象字面量方式创建单例对象

var singleton = {
    name:value;
    method:function(){
    }
};

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

var singleton = function(){
    var privateVar = 10;
    function privateFunction(){
        return false;
    }

    return {
        publicProperty:true,
        publicMethod:function(){
            ...
        }
    }
};

以上代码返回一个对象字面量,对象字面量里的函数有权访问私有变量和函数,在外部可以通过singleton.publicMethod()这种形式访问
从本质上讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的(类比java中的单例模式)

var application = function(){
    var components = new Array();
    //初始化
    components.push(new BaseComponent());//这两个语句在第一次执行后就不再执行,因为外面只会使用return的两个方法访问该私有变量
    return {
        getComponentCount:function(){
            return components.length;
        },
        registerComponent:function(component){
            if(typeof component == "Object"){
                components.push(component);
            }
        }
    };
}();

<h3 id="7.4.3">7.4.3 增强的模块模式</h3>

适合一些单例必须是某种类型的实例,同时必须添加某些属性或方法对其加以增强的情况

var singleton = function(){
    var privateVar = 10;
    function privateFunction(){
        return false;
    }

    var object = new CustomType();
    object.publicProperty = true;
    object.publicMethod = function(){
        ...
    }
    return object;
}();

以上代码要求singleton对象必须是CustomType的实例(区别就是把公共属性和方法定义到CustomType实例中了)

<h2 id="7.5">7.5 小结</h2>

  • 函数表达式的特点:

    • 函数声明要有名字,但函数表达式不需要。即函数表达式可以是匿名函数
    • 在无法确定如何引用函数的情况下,递归函数会变得比较复杂
    • 递归函数应该始终使用arguments.callee来递归地调用自身,不要使用函数名——函数名可能会发生变化
  • 闭包:在函数内部定义其他函数时,就创建了闭包,闭包有权访问包含函数内部的所有变量:

    • 闭包的作用域链包含着自己的作用域、包含函数的作用域和全局函数的作用域
    • 通常函数的作用域及其所有变量都会在函数执行结束后被销毁
    • 但是当函数返回了一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止
  • 使用闭包模仿块级作用域

    • 思路就是创建并立即调用一个函数,这样会立即执行里面的代码,又不会在内存中留下对该函数的引用
    • 结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域中的变量
  • 使用闭包创建私有变量:

    • JS中没有正式的私有对象的概念。这里的私有对象是指在外部访问不到的变量。可以用闭包来实现公有方法,从而访问到在包含作用域中定义的变量
    • 有权访问私有变量的公有方法叫特权方法
    • 自定义类型的特权方法有:构造函数模式、原型模式。单例的特权方法有:模块模式、增强的模块模式

推荐阅读更多精彩内容

  • You don't KnowJS 引语:你不懂的JS这本书�github上已经有了7w的star最近也是张野大大给...
    Sleet阅读 148评论 0 0
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 552评论 2 6
  • 定义 定义一:有权访问另一个函数作用域中的变量的函数。定义二:可以访问其被创建时所处的上下文环境的函数。 创建闭包...
    大橙子CZ阅读 83评论 0 0
  • 谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至...
    sponing阅读 345评论 0 7
  • 天光大好, 我想给你一个拥抱, 不能弥补所有的过错, 你还在我身边, 就好。 天光大好, 我想要你一个拥抱, 只求...
    帝司阅读 61评论 0 1