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有问题欢迎与我讨论,共同进步。

  • 相关阅读:
    WPF 使用 Direct2D1 画图 绘制基本图形
    WPF 使用 Direct2D1 画图 绘制基本图形
    dot net core 使用 IPC 进程通信
    dot net core 使用 IPC 进程通信
    win2d 图片水印
    win2d 图片水印
    Java实现 LeetCode 240 搜索二维矩阵 II(二)
    PHP closedir() 函数
    PHP chroot() 函数
    PHP chdir() 函数
  • 原文地址:https://www.cnblogs.com/starof/p/6400261.html
Copyright © 2011-2022 走看看