zoukankan      html  css  js  c++  java
  • javascript 关于闭包的知识点

    javascript 关于闭包的认识

      概念:闭包(closure)是函数对象与变量作用域链在某种形式上的关联,是一种对变量的获取机制。

          所以要大致搞清三个东西:函数对象(function object)、作用域链(scope chain)以及它们如何关联(combination)

      首先要建立一个印象,在js中,几乎所有的东西可以看作对象,除了null和undefined。比如常用的数组对象、日期对象、正则对象等。

        var num = 123; // Number
        var arr = [1,2,3]; // Array
        var str = "hello";  // String

      数字原始值可以看作Number对象,字符串原始值可看做String对象,数组原始值可看作Array对象。有的原始值还可直接调方法,如数组、正则,有的不行

        [1,2,3].toString();  // 可以
        /w+/.toString();  // 可以
        123.toString();  // 报错

      比如数字,当用一个变量名呈接它时,或者加上括号,能够这样用

        var num = 123;
        num.toString();
        123.toString();  // 报错,因解释器当成浮点数来解读
        (123).toString();  // 可以

      所以,函数也可以是一个对象,函数细说起来又要扯一大堆,毕竟是js精华,简单说这里有用的。函数的定义常见的是

    复制代码
        function fun1(val){  }
        fun1(5);  
    
        var f1 = function(val){  };  // 定义一个函数赋给变量
        f1(6);
        var f2 = function fun2(){ }; // 或者取一个函数名
        f2();
    复制代码

      因为函数也是对象、变量,所以可以赋给一个变量(更准确说是赋给左值),在这个变量后面使用()调用运算符就可以调用这个函数了,如f1()。还有一种方式:定义即调用

        var num = (function(val){ return val * val; }(5)); // num为25

      在定义一个函数体后面加上()和参数(或者没有参数),就是对这个定义的函数进行了调用,直接传入参数5计算并赋值给num,因此num是一个数值变量而不是函数变量。

          既然函数是对象,当然也有new关键字的表达式

    复制代码
        var a = new Array(1,2,3);  // 数组的对象创建表达式
        var f = new Function("x", "y", "x = 2 *x; return x + y;");  // 函数对象创建表达式
        
        var f1 = function(x, y){  // 函数f的函数体类似f1的定义
              x = 2 * x;
              return x + y;
        };
    复制代码

      函数对象的创建使用了Function关键字,前面的参数均被当做对象创建函数的形参,如这里的x、y,最后一个字符串是函数的函数体,多行函数体仍以;相隔。

         很多时候特别是使用jQuery时经常看到函数调用时传递函数,大多数时候直接写匿名函数,也可可传递一个函数变量

        func(index, function(val){
               /* 匿名函数 */
        });
        var f = function(val){ /* 函数变量 */ };
       func1(index, f);

      既然函数函数是对象,可以赋给一个变量,自然也可以作为返回值了,而且在js中,函数可以嵌套定义。

    复制代码
        function func(){
            return function(x){  // 返回一个函数变量
                return x * x;
            }
        }
        var f = func();
        f(5);  // 对这个函数进行调用
    function func1(){ function nested1(){ } // 嵌套定义函数 function nested2(){ } }
    复制代码

      对函数有个大致了解,说说变量作用域问题,有几个原则:

           1. 全局变量拥有全局作用域.

           2. 局部变量(一般指定义在函数内部)拥有局部作用域(包括其嵌套的函数).

           3. 在局部变量若跟全局变量重名,优先使用局部变量.

        val = 'value';  // 变量定义可以不用var关键字
        document.write("val=>" + val + "<br/>");
    
        // g是全局变量,在全局作用域中有效,所以在给g初始化之前就可以访问,只是值是undefined
        document.write("g=>" + g );
        var g = 'google';

      全局变量的定义,相当于是全局对象的属性,一般我们用this指代这个全局对象,比如在浏览器中运行的时候,它指的是window对象,即当前窗体,在全局作用域中,以下三种访问形式等效

        var g = 'google';
        document.write("g=>" + g + "<br/>");
        document.write("g=>" + this.g + "<br/>");
        document.write("g=>" + window.g + "<br/>");

      而在函数定义内部访问变量时,遵循同名优先,函数作用域内部总是优先访问,例如

    复制代码
        var scope = "global";
        function func1(){
            var scope = "local";
            console.log(scope);  // local
        };
        func1();
        function func2(){
            scope = 'changed global';  // 如不加var,改变的是全局变量的值
        };
        func2();
        console.log(scope);  // changed global
    复制代码

      比较有意思的地方是,如果在一个函数内部给一个全局变量赋值时没有加var关键字,如func2,它改变的是全局作用域变量的值!而前面说的this,也有个有意思的地方

    复制代码
        var scope = "global";
        function func(){
            var scope = "local"
            console.log(scope); // local
            console.log(this.scope);  // global
        }
        func();
    复制代码

      在局部作用域(这里均指函数内部),如果有同名变量,以this引用的话,结果是全局变量,即,在函数内部(注意不是方法,方法一般指对象的属性方法),this指代的是全局的对象。再看一个

    复制代码
        var scope = "global";
        function func(){
            "use strict";   // 开启ECMAScript5严格模式
            console.log(this);  // undefined
            console.log(this.scope);  // 报错:TypeError: scope undefined
        }
        func();
    复制代码

      在严格模式中,对语法检查更加严格。在一个全局作用域定义的普通函数中,log打印this是undefined,所以this引用scope当然也不存在。

          在ECMAScript(为js脚本制定的一个标准)中,强制规定全局作用域定义的变量,是全局对象(比如在浏览器客户端运行时为window,默认的全局中的this关键字也是指它,通常我们说的就是这个)的属性。一般我们把跟某个变量作用域相关的对象为上下文环境(context),比如全局对象this只要涉及环境这类偏底层的东西肯定就是编程语言层面自己规定的。

      但这个全局对象this限于非严格模式的情况。js允许我们在局部变量的环境中(函数)以this引用全局对象,在严格模式下却没法这样干,也许是它把这个对象给隐藏了,至少目前这样写会报错。

      正是js的函数有局部作用域的特殊功能---全局作用域无法访问函数中定义的变量,所以在js中,也用函数规定命名空间,相比其他有的语言使用的是namespace关键字,js目前好像是把namespace作为保留字,你的变量的命令不能跟它重名,但没投入使用。

        (function(){
            var name = "Jeff";
            var age = 28;
            var pos = "development";
        }());  // 在函数中定义一堆变量,外边无法访问

      在一个函数中定义一堆变量,当然得调用它才能生效,这堆变量就限于在这个空间内使用了,好像用得也不多。

      一个重要的点,一个函数中规定的变量,在这个函数内部所有地方都可访问,包括嵌套函数。所以可以出现下面这个现象

        function func(){
            console.log(num);  // undefined,先于定义访问
            var num = 123;
            console.log(num);  // 123
        }
        func();

      这种特性有时被称为声明提前(hoisting),相当于这样

        function func(){
            var num;
            console.log(num);  // undefined,先于定义访问
            var num = 123;
            console.log(num);  // 123
        }

      然后是嵌套函数,只要在函数内定义的,该函数内均能访问,看例子

    复制代码
        function func(){
            var str = "hello";
            function nested1(){  // 第一次嵌套
                str = "nested1";
                function nested2(){ str = "nested2"; }  // 第二次嵌套
                nested2();
            }
            nested1();
            console.log(str);
        }
        func();  // 打印nested2
    复制代码

      除了明确在函数内定义的变量,还有定义函数时的形参,它们在整个函数内也是可访问的。

      现在我们大概了解函数也是对象,以及全局、局部作用域,在全局作用域定义的变量,是对应的全局对象的属性,也就是说有个全局对象关联它,就从这点进入作用域链(scope chain)吧,这是理解闭包的基础。

      在一个局部作用域内,或者说定义的函数,想象它们关联着某个对象,这个对象是随着我们定义这个函数而自动生成的,函数内定义的变量以及函数的形参均是这个对象的属性,所以在这个对象内部总是可以顺利访问到它们,类似于全局变量是全局对象的属性。这种对象关联在定义时就已经决定了,而不是在调用时才形成(这很重要)。

          但是我们定义的很多函数都是嵌套的,由外到内每个函数都会有一个对应的自定义对象跟它关联

    复制代码
        var a = "a";
        var name = "Michel";
        function fun(b){
            var c = "c";
            var name = "Clark";
            function nested(d){
                var name = "Bruce";
                var e = "e";
                /* TODO */
            }
            /* TODO */
        }
    复制代码

      在上面的嵌套函数nested生成一个自定义对象时,fun函数、全局作用域也会生成对象,因此它们可以形成一个对象的列表或者链表,简单的将函数名作为对象名,全局对象用global表示,并且从内嵌函数nested函数出发的话,大概是这样:

        

      列表上的一组对象定义了这段代码作用域中定义过的变量:即它们的属性。第一个对象的属性是当前函数的形参与内定义变量,第二个对象是它的外部函数的形参与内定义变量,一直到最后是全局对象和它的属性---全局定义的变量,也就是说,当前函数永远在这个列表的最前面,这样才可以保证该函数范围内的变量总是具有最访问高优先级。每次访问变量时便会顺着这个列表查找,这被称为变量解析(variable resolution),如果一直找到列表末尾都找不到对象中的这个属性,会抛一个ReferenceError错误。

      每个定义的函数对象都会有类似这样一个列表与之关联,它们之间通过这个作用域链相关联,而函数体内定义的变量均可保存在函数作用域内,这是在函数定义时及确定的,这种特性称之为闭包。      通常直接把函数称作闭包,而且理论上来说所有的js函数都是闭包,因为它们都是对象,闭包这种机制让js有能力来“捕捉”变量。第一个例子:

    复制代码
        var scope = "global";
        function func(){
            var scope = "local";
            function nested(){ return scope; }
            return nested();  // 调用并返回scope
        }
        console.log(func());
    复制代码

      常规的定义和调用,嵌套函数的定义并调用在局部作用域中完成。它打印的是local。再看这个

    复制代码
        var scope = "global";
        function func(){
            var scope = "local";  // 局部变量
            function nested(){ return scope; }
            return nested;  // 返回这个嵌套函数变量
        }
        console.log(func()());  // 在全局作用域中调用局部的嵌套函数
    复制代码

      前面说过,函数也是变量、对象,也可以作为函数返回值,func不直接返回变量,而返回一个内嵌函数,然后在全局作用域调用这个局部内嵌函数,它会返回什么呢?结果仍为local。由于闭包机制,nested函数在定义时就已经决定了,函数体内的scope变量值是local,这是一种绑定关系,不会随着调用环境的改变而改变,它去对象关联列表中查找按优先级分的话,总是func函数对应的scope值。

      闭包的功能很强大,看这个例子(例A

    复制代码
        var integer = (function(){
                var i = 0;
                return function(){ return i++; };
            }());
        console.log(integer());  // 0
        console.log(integer());  // 1
        console.log(integer());  // 2
    复制代码

      如果有点C/C++基础的人,初看这个调用结果,很可能会说这个打印的是0,0,0,反正我是很小白的这样想,每次调用完,临时变量i就会被销毁。但是确实有打印0的情况,看下这个(例B

    复制代码
        function func(){
            var i = 0;
            function nested(){ return i++; }
            return nested();
        }
        console.log(func());  // 0
        console.log(func());  // 0
        console.log(func());  // 0
    复制代码

      也就是说,这两个看起来差不多的函数还是有点差别的。

         在C/C++中,如果不是全局变量或局部静态变量,只要在局部函数中定义的变量在调用一次完成后就马上被销毁,当然除使用malloc、realloc、new等函数开辟的动态空间除外,这种必须得手动释放,否则容易造成内存泄露。js中是垃圾自动回收机制,某些无用的东西会被自动销毁,在前两个例子中,例A显然没有被销毁,而例B中的变量被销毁了,因为每次调用都是新声明一个i变量。so why?

      在C中,局部变量被临时保存在一个栈中,先调用的先入栈,后调用的后入栈,调用完从栈订弹出,变量内存被销毁,利用的是栈的后进先出特点。而js依靠的是作用域链,这是一个列表或者链表,并不是栈,没有所谓的压入(push)、弹出(pop)操作,如果说定义时就有一个列表的话,每次调用一个函数时,都会创建一个新的、跟它关联的对象,保存着局部变量,然后把这些对象添加至一个列表中形成作用域链,即便调用同一个函数两次,生成也是两个列表。

          当一个函数执行完要返回的时候,便把对应对象从列表中删除,对象中的属性也会被销毁,意味着局部函数中的变量将不复存在,在例B中,return nested(),执行完返回一个值,nested函数再无任何作用,被从列表中删掉了。

      如果一个局部函数定义了嵌套函数,并且有一个外部引用指向这个嵌套函数,就不会被当做垃圾回收。什么时候会有一个外部引用指向它(内嵌函数)?当它作为返回值(即返回一个函数变量),或者它作为某个对象属性的值存储起来时。不会被当成垃圾回收,它绑定的对象也不会从对象列表中删掉,这个绑定对象的属性和值自然也不会被销毁,自然可以进行复用了。所以再次调用其创建一个新的对象列表时,变量的值是在上一次调用的基础上改变的。

      例A返回的是一个函数变量,意味着有一个外部引用指向着它:Hey!你可能会被调用哦,我不会删除你的。这样每次i是累加的。类似例A,如果将函数保存为一个对象的属性也不会被删除,例C

    复制代码
        function counter(){
            var n = 5;
            return {
                count: function(){ return n++; },
                reset: function(){ n = 0; }
            };
        }
        var countA = counter();
        console.log(countA.count());  // 5
        console.log(countA.count());  // 6
        console.log(countA.count());  // 7
    复制代码

      例C返回一个对象,对象的属性均是函数,也符合上边的情形,所以n值是累加的。这些情况下,通常我们会把类似定义的n称为这个外部函数的私有属性(成员),因为它们运行起来就像是函数的内嵌函数(闭包)共享的东西一样。前面说过,我们有时直接将函数称作闭包,尤其是同一个函数内部定义的函数

    复制代码
        function func(){
            var funArr = [];
            for(var i = 0; i <= 2; ++i)
                funArr[i] = function(){ return i; };
            return funArr;
        }
        var farr = func();
        console.log(farr[0]());
        console.log(farr[1]());
        console.log(farr[2]());
    复制代码

      可以说:Here we create three closures, they are all defined in one function, so they share access to the variable i。那么它们输出神马?事实是,它们都打印3。它们共享变量,当func执行完时,i的值为3,所有的闭包均共享这个值。这个例子说明闭包的一个重要特点:所有闭包在与作用域链关联时是“活动的”,虽然函数一定义完成,作用域链就随着生成,但是所有闭包均不会单独对作用域内的私有成员(如上例中的i、例C中的n)进行复制一份,不会生成一个静态快照,而是共享,当这个成员的值改变时,它们返回的值也跟着变化。

  • 相关阅读:
    Zabbix5 Frame 嵌套
    Zabbix5 对接 SAML 协议 SSO
    CentOS7 安装 Nexus
    CentOS7 安装 SonarQube
    GitLab 后台修改用户密码
    GitLab 查看版本号
    GitLab Admin Area 500 Error
    Linux 安装 PostgreSQL
    Liger ui grid 参数
    vue.js 是一个怪东西
  • 原文地址:https://www.cnblogs.com/lala314/p/4984759.html
Copyright © 2011-2022 走看看