Javascript的闭包

作者: danejahn | 来源:发表于2016-05-05 16:51 被阅读83次

    对js的广大初学者来说,闭包绝对是个难点。而且经常出现今天感觉懂了,明天就又不懂了的情况。本文就尝试从我自己的学习体会出发,尝试把这个概念讲清楚。
    简单来说,闭包是指有权访问另一个函数作用域中的变量的函数
    下面这个函数是一个根据初始值自加的函数。

    function count(init) {
    
        return function() {
            init++;
            return init;
        }
    }
    
    var f1 = count(1);
    console.log(f1());  //2
    console.log(f1());  //3
    
    var f2 = count(11);
    console.log(f2());  //12
    console.log(f2());  //13
    

    上面就是一个闭包的例子。count函数在执行完之后返回了内部匿名函数,并赋值给f1和f2,f1和f2依然可以访问count函数中init变量,f1和f2就是两个闭包。
    要搞清楚其中的细节,我们就必须理解f1和f2在第一次调用的时候到底发生了什么。我们首先来看两个基本观念:执行环境及作用域。

    执行环境及作用域

    执行环境

    执行环境(execution context,有时直接简称为“环境”)是ECMAScirpt中最为重要的一个概念,用来描述js代码执行的抽象概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。换句话说,所有的js都是在某个执行环境中运行的,我们可以把执行环境想成一个执行js代码的盒子。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
    全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境的不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
    每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入到环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。

    作用域链

    当js代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端, 始终是当前执行代码所在环境的变量对象. 如果这个环境是一个函数, 则将其活动对象(activation object)作为变量对象. 活动对象在最开始时只包含一个变量, 即arguments对象(这个对象在全局环境中是不存在的). 作用域链中的下一个变量对象来自包含(外部)环境, 而再下一个变量对象则来自下一个包含环境. 这样一直延续到全局执行环境.
    标识符解析是沿着作用域链一级一级地搜索标识符的过程. 搜索过程始终从作用域链的前端开始, 然后逐级地向后回溯, 直至找到标识符为止(如果找不到标识符, 通常导致错误发生)

    闭包

    我们再来看看我们的demo

    function count(init) {
    
        return function() {
            init++;
            return init;
        }
    }
    
    var f1 = count(1);
    console.log(f1());  //2
    console.log(f1());  //3
    

    f1之所以还能访问 变量 init, 是因为f1函数的作用域链包含 count函数的作用域.
    下面是最关键的部分:

    1. 在创建count()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。
    2. 当调用count()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链. 此后, count()函数的活动对象被创建, 并被推入到执行环境作用域链的前端.
    3. 在count()函数内部的匿名函数会将count()函数的执行环境的作用域链初始化成自己的作用域链中. 这样匿名函数就可以访问count()函数中的所有变量了.
    4. 当count()函数中的匿名函数最终返回并赋值给f1, f1的作用域链就包含全局变量对象和count()函数的活动对象, 所以count()函数的活动对象不会被销毁. 换句话说, count()函数执行完毕后, count()函数的执行环境被销毁, 但是count()函数的活动对象直到f1被销毁后, 才会被销毁.

    到这里我们就明白了, 只要你在一个函数内部定义了另一个函数, 闭包就产生了.

    this对象

    在闭包中使用this对象会遇到一些问题. 我们知道this对象指向了当前代码的执行环境. 也就是说, 在全局环境中this等于window(浏览器环境), 当被当做某个对象的方法调用时, this指向的就是那个方法.

    当然, 也可以通过apply()和call()改变函数的执行环境

    我们看一下下面的例子:

    var name = "The Window";
    
    var object = {
        name : "My Object",
    
        getNameFunc : function () {
            return function () {
                return this.name;
            };
        }
    };
    
    console.log(object.getNameFunc()());
    

    这时候return回来的是"The Window", 而不是"My Object"
    我们分解一下来看:

    1. object.getNameFunc()执行时, getNameFunc()是作为object的方法执行的, this指向object, 然后返回一个匿名函数.
    2. 这个匿名函数在调用的时候, 实际上是在全局环境中执行的, 所以this指向全局环境, 返回this.name就是"The Window"

    如果我们想返回"My Object"该咋办? 那我们就得想着怎么把第一步中的this传到第二步的匿名函数中.

        getNameFunc : function () {
            var that = this;
            return function () {
                return that.name;
            };
        }
    

    在定义匿名函数前, 我们把this保存在that变量中, 这样闭包也可以访问that变量.

    模仿块级作用域

    我们知道Javascript中没有块级作用域, 也就是定义块中变量, 它的作用域是当前函数, 和块没有关系. 我们可以利用函数的作用域来模仿块级作用域.

    !function() {
        var i = 10;
        console.log(i); //10
    }();
    
    console.log(i+1);   //i is not defined
    

    我们创建了一个函数并立即调用它, 这样其中的代码执行了, 而且因为函数执行完毕, 它的执行环境和其中的变量对象都会被销毁, 所以下面的代码提示i is not defined

    封装

    面向对象的三大基石之一就是封装. 封装简单来说就是只公开代码单元的对外接口, 而隐藏内部的具体实现.
    Javascript是面向对象的语言, 那它如何实现封装呢? 我们知道Javascript中没有私有成员的概念, 所有对象的属性都是公开的. 但是呢, Javascript有私有变量的概念, 函数内部的变量外部是无法访问的. 这里, 我们就可以利用闭包来完成封装.

    function Account() {
        var balance = 0;
        function save(money){
            balance += money;
            query();
        }
    
        function draw(money){
            if(money > balance){
                balance = 0;
            }
            else{
                balance -= money;
            }
            query();
        }
        
        function query(){
            console.log("Your balance is " + balance);
        }
    
        return {
            Save : function(money){
                save(money);
            },
            Draw : function(money){
                draw(money);
            }
        }
    }
    
    var acount = new Account();
    
    acount.Save(10);
    acount.Draw(5);
    
    acount.save(10);    //save is not a function
    console.log(acount.balance);    //undefined
    

    例子是个银行账户对象, 对外公开了存钱和取钱两种操作. 这里用工厂模式来创建对象, 用构造函数也是同样的道理. 我们把有权访问私有变量和方法的公有方法成为特权方法(Save和Draw方法)

    呼呼, 好像我想说的都说完了, 下面开始一分钟满分作文时间, 来回顾一下我们都学到了什么:

    • 当在函数内部定义了其他函数时, 就创建了闭包. 闭包有权访问函数内部的所有变量.
      -闭包的作用域链, 包含着自己的作用域, 包含函数的作用域和全局的作用域
      -通常, 函数的作用域和变量会在函数调用结束后销毁.
      -但是, 当函数返回了闭包时, 函数的作用域会一直保存直到闭包不存在为止
    • 创建并立即调用函数可以模仿块级作用域
    • 闭包可以实现封装

    相关文章

      网友评论

        本文标题:Javascript的闭包

        本文链接:https://www.haomeiwen.com/subject/kslplttx.html