zoukankan      html  css  js  c++  java
  • javascript 函数和作用域(闭包、作用域)(七)

    一、闭包

    JavaScript中允许嵌套函数,允许函数用作数据(可以把函数赋值给变量,存储在对象属性中,存储在数组元素中),并且使用词法作用域,这些因素相互交互,创造了惊人的,强大的闭包效果。【update20170501】

    闭包就是指有权访问 另一个函数作用域 中的变量 的函数 !!!

    好处:灵活方便,可封装

    缺点:空间浪费、内存泄露、性能消耗

    由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,建议只在绝对必要时再考虑使用闭包。虽然像V8等优化后的JavaScript引擎会尝试回收被闭包占用的内存,还是要慎重使用闭包。

    1、原理分析[update20170322]

    无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况有所不同。

    例:以此为例说明闭包原理

    function createComparisonFunction(propertyName){
        return function(object1,object2){
            //匿名函数中value1和value2访问了外部函数中的变量propertyName
            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:"Nicholas"},{name:"Gerg"});       //1
    
    //解除对匿名函数的引用(以便释放内存)
    compareNames=null;

    即使内部函数(匿名函数)被返回了,而且在其他地方被调用了,它仍然可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含外部函数createComparisonFunction()的作用域。

     在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。

    在匿名函数从createComparisonFunction()中返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局对象。

    这样匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。

    换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁。

    2、简单例子

    一般函数执行完后局部变量释放,有闭包则局部变量不能在函数执行完释放。

    例1:

    调用outer()返回匿名函数,这个匿名函数仍然可以访问外部outer的局部变量localVal,所以outer执行完成后localVal不能被释放。

    outer()调用结束,func()再次调用的时候仍然能访问到外层的outer()这个外函数的局部变量。这种情况就是通常所说的闭包。

    例2:【update20170307】

    //创建一个名为quo的构造函数
    //它构造出带有get_status方法和status私有属性的一对象。
    var quo=function(status){
        return{
            get_status:function(){
                return status;
            }
        }
    }
    //构造一个quo实例
    var myQuo=quo("amazed");
    document.writeln(myQuo.get_status());//amazed

    quo函数被设计成无须在前面加上new来使用,所以名字也没有首字母大写。调用quo时,它返回包含get_status方法的一个新对象。该对象的一个引用保存在myQuo中。即使quo已经返回了,但get_status方法仍然享有访问quo对象的status属性的特权。get_status方法并不是访问该参数的一个副本,它访问的就是该参数本身。这是可能的,因为该函数可以访问它被创建时所处的上下文环境。这被称为闭包。

    3、前端闭包

    例1:定义一个函数,它设置一个DOM节点为黄色,然后把它渐变为白色

    var fade=function(node){
        var level=1;
        var step=function(){
            var hex=level.toString(16);
            node.style.backgroundColor='#FFFF'+hex+hex;
            if(level<15){
                level+=1;
                setTimeout(step,100);
            }
        }
        setTimeout(step,100);
    }
    
    fade(document.body);//调用fade,把document.body作为参数传递给它(HTML<body>标签所创建的节点)

    fade函数设置level为1,。它定义了一step函数;接着调用setTimeout,并传递step函数和一个时间(100毫秒)给它。然后setTimeout返回,fade函数结束。

    大于十分之一秒后,step函数被调用。它把fade函数的level变量转化为10位字符。接着,它修改fade函数得到的节点的背景颜色。然后查看fade函数的level变量。如果背景色尚未变白色,那么它增大fade函数的level变量,接着用setTimeout预定它自己再次运行。

    step函数很快再次被调用。但这次,fade函数的level变量值变成2。fade函数在之前已经返回了,但只要fade的内部函数需要,它的变量就会持续保留。

    例2:

    点击事件里面用到外层的局部变量,有了闭包在数据的传递上更为灵活。

    !function(){
        var localData="localData here";
        document.addEventListener('click',
            function(){
                console.log(localData);
        });
    }();

    异步请求,用$.ajax()方法,在success回调中,用到外层的这些变量。在前端编程中,经常直接或间接,有意或无意用到闭包。

    !function(){
        var localData="localData here";
        var url="http://www.baidu.com";
        $.ajax({
            url:url,
            success:function(){
                //do sth
                console.log(localData);
            }
        });
    }();

    4、常见错误—循环闭包

    闭包作用域链的机制引出的一个问题:闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。

    例1:

    createFunctions()函数返回一个函数数组,表面看每个函数都返回自己的索引值。实际上,每个函数都返回10。

    因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。但createFunctions()函数返回后,变量i的值是10,此时每个函数都引用这保存变量i的同一个变量对象,所以每个函数内部i的值都是10。

    正确方法:通过创建另一个你们函数强制让闭包的行为符合预期。

    function createFunctions(){
        var result=new Array();
        for(var i=0;i<10;i++){
            result[i]=function(num){
                return function(){            
                    return num;
                }
            }(i);
        }
        return result;
    }

    例2:

    期望结果:点击aaa弹出1,点击bbb弹出2,点击ccc弹出3。

    <script>
        document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>";
        for(var i=1;i<4;i++){
            document.getElementById('div'+i).addEventListener('click',function(){
                alert(i);//all are 4!!!
            });
        }    
    </script>

    这段代码执行后无论点击哪个,弹出的永远是4。

     

    因为事件处理器函数绑定了变量i本身,而不是函数在构造时的变量i的值。

    addEventListener里面是个回调函数, 当点击的时候,这个回调函数才会动态的拿到i的值,在整个初始化完成之后i的值就已经是4了。

    正确做法:

    在每次循环的时候用一个立即执行的匿名函数包装起来,每次循环的时候把i的值传到匿名函数里面,在匿名函数里面再去引用i。这样的话,在每次点击alert的函数i会取自每一个闭包环境下的i,这个i来源于每次循环时的赋值i,这样的话才能实现点击弹出1,2,3的次序。

    document.body.innerHTML="<div id=div1>aaa</div>"+"<div id=div2>bbb</div><div id=div3>ccc</div>";
        for(var i=1;i<4;i++){
    
            !function(i){
                document.getElementById('div'+i).addEventListener('click',function(){
                alert(i);//right
                });    
            }(i);
        }

    5、闭包和this对象[update20170322]

    在闭包中使用this对象也可能会导致一些问题。

    this对象是在运行时基于函数的执行环境绑定的:

    • 在全局函数中,this等于window
    • 函数作为某个对象的方法调用时,this等于那个对象。
    • 匿名函数的执行具有全局性,因此其this对象通常指向window。

    有的时候,由于编写闭包的方式不同,匿名函数的this指向window这一点可能不会那么明显。

    var name="The Window";
    var object={
        name:"My Object",
        getNameFunc:function(){
            'use strict';
            return function(){
                return this.name;
            }
        }
    }
    console.log(object.getNameFunc()());//The Window (非严格模式)

    object包含一个name属性,还包含一个方法—getNameFunc(),返回一个匿名函数,而匿名函数又返回this.name。

    由于getNameFunc()返回一个函数,因此调用object.getNameFunc()()就会立即调用它返回的函数,结果就是返回一个字符串。

    这个例子返回的字符串是“The Window”,即全局name变量的值。为什么匿名函数没有取得其包含作用域(或者外部作用域)的this对象呢?

    每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到内部函数自己的活动对象为止,可以看上面的原理图,因此永远不可能直接访问外部函数中的这两个变量。

    可以把外部作用域的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

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

    在几种特殊情况下,this的值可能会意外地改变。下面是代码修改前不同调用方式下的结果。

    var name="The Window";
    var object={
        name:"My Object",
        getName:function(){//getName()方法简单地返回this.name的值
            return this.name;
        }
    }
    //几种不同调用object.getName()的方式
    console.log(object.getName());//My Object
    console.log((object.getName)());//My Object
    console.log((object.getName=object.getName)());//The Window
    •  object.getName()普通调用
    • (object.getName)()调用getName()方法前先给它加上了括号。虽然加上了括号之后,就好像只是在引用一个函数,但this的值得到了维持,因为object.getName和(object.getName)的定义是相同的。

    • (object.getName=object.getName)()先执行一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持,结果就返回了“The Window”。

    6、闭包的好处—封装

    封装再具体一点:

    • 模仿块级作用域
    • 私有变量

    (function(){})() 里面定义一些想让外部无法直接获取的变量_userId,_typeId,最后通过window.export=export把最终想输出的对象输出出去。

    <script>
        (function(){
            var _userId=23492;
            var _typeId='item';
            var myExport={};
    
            function converter(userId){
                return +userId;
            }
    
            myExport.getUserId=function(){
                return converter(_userId);
            }
    
            myExport.getTypeId=function(){
                return _typeId;
            }
            window.myExport=myExport;
        })();
    
    
        console.log(myExport.getUserId());  //23492
        console.log(myExport.getTypeId());   //item
        console.log(myExport._userId);//undefined
        console.log(myExport._typeId);//undefined
        console.log(myExport.converter);//undefined
    </script>

    对应外部使用export对象上的getUserId()方法的人来说,只能通过export上提供的方法来间接访问到具体的函数里面的变量,利用了闭包的特性,getUserId在函数执行完了后仍然能访问到里面的自由变量。

    在函数外面无法通过myExport._userId直接访问变量,也没法去改写变量。

    二、作用域

    1、全局函数eval作用域

    比较简单。有时候也经常引起误解。有哪几种作用域:全局、函数和eval作用域。

    2、作用域链

     闭包outer1里可以访问到自由变量local2也可以访问到global3。

    function outer2(){
            var local2=1;
            function outer1(){
                var local1=1;
                //可以访问到 local1,local2 or global3
                console.log(local1+','+local2+','+global3);
            }
            outer1();
        }
        var global3=1;
        outer2();//1,1,1

    3、利用函数作用域封装

     如果没有一些模块化的工具的话,经常看到很多类库或者代码最外层,去写一个匿名函数如下:

    (function(){
        //do sth here
        var a,b;
    })();

    或者

    !function(){
        //do sth here
        var a,b;
    }();

    或者

    +function(){
        //do sth here
        var a,b;
    }();

    好处:把函数内部的变量变成函数的局部变量,而不是全局变量,防止大量的全局变量和其他代码或者类库冲突。

    用!或者+目的是把函数变成函数表达式而不是函数声明。如果省略掉!,把一个完整的语句以function开头的话,会被理解为函数声明,会被前置处理掉,最后留下一对括号或者函数声明省略了名字的话都会报语法错误。

    三、ES3执行上下文(可选)【update20170321】

    执行环境(execution context)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

    每一次函数调用的时候,都有一套执行环境(execution context)。

    某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也会随之销毁(全局执行环境直到应用程序退出—例如关闭网页或浏览器—时才会被销毁)。

    抽象概念:执行上下文,变量对象

    1、执行上下文

    类似一个栈的概念。

    函数调用1万次就会有1万个Execution context执行上下文。

    每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。

    console.log('EC0');
    
    function funcEC1(){
        console.log('EC1');
        var funcEC2=function(){
            console.log('EC2');
            var funcEC3=function(){
                console.log('EC3');
            }
            funcEC3();
        }
        funcEC2();
    }
    
    funcEC1();
    //EC0 EC1 EC2 EC3

    控制权从EC0到EC1到EC2到EC3,EC3执行完后控制权退回到EC2,EC2执行完之后控制权退回到EC1,EC1执行完后退回到EC0

    2、变量对象

    JavaScript解释器如何找到我们定义的函数和变量?

    需要引入一个抽象名词:变量对象。

    变量对象(Variable Object,缩写为VO)是一个抽象概念中的“对象”,它用于存储执行上下文中的:1、变量2、函数声明3、函数参数。

    例子:比如有一段javaScript代码

    var a=10;
    function test(x){
        var b=20;
    }
    test(30);

    全局作用域下的VO等于window,等于this。

    当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数有序访问。

    作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

    3、全局执行上下文(浏览器)

    全局执行环境(执行上下文)是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在web浏览器中,全局执行环境是window对象。因此所有全局变量和函数都是作为window对象的属性和方法创建的。

    在JavaScript第一行就可以调用Math,String,isNaN等方法,在浏览器里也可以拿到window,为什么?

    因为在全局作用域下,背后就有一个变量对象VO(globalContext)===[[global]];

    在第一行代码执行之前,浏览器js引擎会把一些全局的东西初始化到VO里面,比如[[global]]里面有Math方法,String对象,isNaN函数,等,也会有一个window,这个window会指向它这个全局对象本身。

    VO对象是一个标准抽象的概念,对应javascript语言本身,是不可见的,没办法直接访问到,

    比如函数对象的VO是没任何办法拿到的;但是在浏览器里面有一个全局的window会指向它自己,所以在控制台里用window.window.window.window...可以一直嵌套下去可以证明这是个无限循环。

    String(10)背后就是会访问对应的VO对象,也就是[[global]]对象,拿到[[global]]对象的属性String。

    4、函数中的激活对象

    函数稍微特殊一点,函数中还有一个概念叫激活对象。

    函数在执行的时候会把arguments放在AO激活对象中。

    初始化auguments之后呢,这个AO对象又会被叫做VO对象。

    和全局的VO一样,进行其他一些初始化,比如说初始化函数的形参,初始化变量的声明,或者是函数的声明。

    4.1、变量初始化阶段

    目的主要是理解一点:为什么函数和变量的声明会被前置?为什么匿名函数表达式的名字不可以在外面调用?

    对于函数对象的VO来说,分为2个阶段,第一个阶段为变量初始化阶段

    上面说了全局作用域下VO变量初始化会把Math,String等一些全局的东西放进去。在第二个阶段才能更好的执行代码。

    函数的变量初始化阶段会把arguments的初始化,会把变量声明和函数声明放进去。

    具体操作:

    VO按照如下顺序填充:
    1、函数参数(若未传入,初始化该参数值为undefined2、函数声明(若发生命名冲突,会覆盖)
    3、变量声明(初始化变量值为undefined,若发生命名冲突,会忽略)

    注意一点:函数表达式不会影响VO 

    比如上面,var e=function _e(){};中_e是不会放到AO中的。这也是为什么在外面不能通过_e拿到函数对象。

    函数变量初始化的阶段把函数声明d放到了AO中,这也就解释了为什么函数声明会被前置。

    函数声明冲突会覆盖,变量什么冲突会忽略。

    4.2代码执行阶段

    这段代码:

    第一阶段:变量初始化阶段AO如下

    第二阶段:代码执行阶段

    得到

    5、测试一下

    <script>
    console.log(x);        //function x(){}
    
    var x=10;
    console.log(x);//10
    x=20;
    
    function x(){}
    console.log(x);   //20
    
    if(true){
        var a=1;
    }else{
        var b=true;
    }
    
    console.log(a);   //1
    console.log(b);    //undefined
    </script>    

    四、作用域链和执行环境的综合例子

    当函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]])。

    例子:定义了compare()函数,并在全局作用域中调用它。

    function compare(value1,value2){
        if(value1<value2){
            return -1;
        }else if(value1>value2){
            return 1;
        }else{
            return 0;
        }
    }
    
    var result=compare(5,10);

    作用域链本质是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

    第一次调用compare(),会创建一个包含this,arguments,value1和value2的活动对象。全局执行环境的变量对象(包含this,result,compare)在compare()执行环境的作用域链中则处于第二位。

    全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。

    在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一执行环境,然后通过赋值函数的[[Scope]]属性中的对象构建起执行环境的作用域链。

    此后,又有一个活动对象(在此作为变量对象使用)被创建并推入执行环境作用域链的前端。对于这个例子中的compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。

    本文作者starof,因知识本身在变化,作者也在不断学习成长,文章内容也不定时更新,为避免误导读者,方便追根溯源,请诸位转载注明出处:http://www.cnblogs.com/starof/p/6400261.html有问题欢迎与我讨论,共同进步。

  • 相关阅读:
    PAT甲级——A1059 Prime Factors
    PAT甲级——A1058 A+B in Hogwarts
    PAT甲级——A1057 Stack
    hdu2665 主席树模板题
    存两个图论模板
    存两个图论模板
    存两个图论模板
    codevs1080 第一次用ZKW线段树
    codevs1080 第一次用ZKW线段树
    codevs1080 第一次用ZKW线段树
  • 原文地址:https://www.cnblogs.com/starof/p/6400261.html
Copyright © 2011-2022 走看看