美文网首页
javascript之理解闭包

javascript之理解闭包

作者: 道无虚 | 来源:发表于2018-04-28 10:00 被阅读0次

    js之闭包

    1、到底什么是闭包

    闭包已经成为近乎神话的概念,它非常重要又难以掌握,而且还难以定义。

    1.1 古老的定义

    闭包(closure),是指函数变量可以保存在函数作用域内,因此看起来是函数将变量“包裹”了起来。

    那这样说来,包含变量的函数就是闭包。这个说法已淘汰。

    //按照古老定义,包含变量n的函数foo就是闭包
    function foo() {
        var n = 0;
    }
    console.log(n)//Uncaught ReferenceError: n is not defined
    

    1.2 定义一

    闭包是指可以访问其所在作用域的函数。

    那这样说来,需要通过作用域链查找变量的函数就是闭包。也不是常规理解。

    //按照定义一的说法,需要通过作用域链在全局环境中查找变量n的函数foo()就是闭包
    var n = 0;
    function foo() {
        console.log(n); // 0
    }
    foo();
    

    1.3 定义二

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

    那这样说来,访问上层函数的作用域的内层函数就是闭包。

    //按照定义二的说法,嵌套在foo函数里的bar函数就是闭包
    function foo(){
        var a = 2;
        function bar(){
            console.log(a); // 2
        }
        bar();
    }
    foo();
    

    1.4 定义三

    闭包是指在函数声明时的作用域以外的地方被调用的函数。

    在函数声明时的作用域以外的地方调用函数,需要通过将该函数作为返回值或者作为参数被传递。

    1、返回值

    //按照定义三的说法,在foo()函数的作用域中声明,在全局环境的作用域中被调用的bar()函数是闭包
    function foo(){
        var a = 2;
        function bar(){
            console.log(a); //2
        }
        return bar;
    }
    foo()();
    

    function foo(){
        var a = 2;
        return function(){
            console.log(a);//2
        }
    }
    foo()();
    

    2、参数

    //按照定义三的说法,在foo()函数的作用域中声明,在bar()函数的作用域中被调用的baz()函数是闭包
    function foo(){
        var a = 2;
        function baz(){
            console.log(a); //2
        }
        bar(baz);
    }
    function bar(fn){
        fn();
    }
    

    因此,无论通过何种手段,只要将内部函数传递到所在的词法作用域以外,它都会持有对原始作用域的引用,无论在何处执行这个函数都会使用闭包。

    1.5 IIFE

    IIFE(Immediately-Invoked Function Expression)(立即执行函数表达式)是不是闭包呢?

    foo()函数在全局作用域定义,也在全局作用域被立即调用,如果按照定义一的说法来说,它是闭包。如果按照定义二和定义三的说法,它又不是闭包。

    var a = 2;
    (function foo(){
        console.log(a);//2
    })();
    

    1.6 总结

    闭包定义之所以混乱,我觉得与经典书籍的不同解读有关。经典定义是犀牛书的原话,定义二是高程的原话。

    这里参考阮一峰老师的理解,原文链接学习javascript中的闭包

    闭包就是能够读取其他函数内部变量的函数。

    由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

    所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

    他的理解主要从作用或用途去直接的表达。小火柴(参考博文博主)想从原理上去表达闭包函数。

    但,归纳起来就是关于一个函数要成为一个闭包到底需要满意几个条件。

    严格来说,闭包需要满足三个条件:

    • 访问所在作用域;
    • 函数嵌套;
    • 在所在作用域外被调用。

    有些人觉得只满足条件1就可以,所以IIFE是闭包;有些人觉得满足条件1和2才可以,所以被嵌套的函数才是闭包;有些人觉得3个条件都满足才可以,所以在作用域以外的地方被调用的函数才是闭包。

    问题是,谁是权威呢?

    2、IIFE

    IIFE(Immediately-Invoked Function Expression)(立即执行函数表达式)。

    2.1 实现

    函数跟随一对圆括号()表示函数调用。

    //函数声明语句写法
    function test(){};
    test();
    
    //函数表达式写法
    var test = function(){};
    test();
    

    但有时需要在定义函数之后,立即调用该函数。这种函数就叫做立即执行函数,全称为立即调用的函数表达式IIFE(Imdiately Invoked Function Expression)。

    javascript引擎规定,如果function关键字出现在行首,一律解释成函数声明语句。

    1、函数声明语句需要一个函数名,由于没有函数名,所以下面报错。

    //SyntaxError: Unexpected token (
    function(){}();
    

    2、函数声明语句后面加上一对圆括号,只是函数声明语句与分组操作符的组合而已。由于分组操作符不能为空,所以下面报错。

    //SyntaxError: Unexpected token )
    function foo(){}();
    
    //等价于
    function foo(){};
    ();//SyntaxError: Unexpected token )
    

    3、函数声明语句加上一对有值的圆括号,也仅仅是函数声明语句与不报错的组合而已。

    function foo(){}(1);
    
    //等价于
    function foo(){};
    (1);
    

    解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。

    常用的两种办法这样:

    (function(){ /* code */ }()); 
    (function(){ /* code */ })(); 
    

    其他的写法:

    var i = function(){ return 10; }();
    true && function(){ /* code */ }();
    0, function(){ /* code */ }();
    
    !function(){ /* code */ }();
    ~function(){ /* code */ }();
    -function(){ /* code */ }();
    +function(){ /* code */ }();
    
    new function(){ /* code */ };
    new function(){ /* code */ }(); 
    

    2.2 作用域

    对于IIFE来说,通过作用域链来查找变量与普通函数有一些不同的地方。

    2.2.1 with

    with语句中的IIFE会先在with语句中查找,然后再向上查找。在下列代码中,标准浏览器下f()函数和IIFE都返回'bar',但IE10-浏览器中的f()函数返回'abc'。

    var foo = "abc";
    with({
        foo:"bar"
    }){
        function f(){
            console.log(foo);
        };
        (function(){
            console.log(foo);
        })();
        f();
    }
    

    2.2.2 try-catch

    在下列代码中,标准浏览器下f()函数和IIFE都返回'error',但IE10-浏览器中的f()函数返回'10'。

    try{
        var e = 10;
        throw new Error();
    }catch(e){
        function f(){
            console.log(e);
        }
        (function (){
            console.log(e);
        })();
        f();
    }
    

    2.2.3 具名函数表达式

    下面的代码,在标准浏览器中a()函数返回1,而IIFE返回a函数代码。但IE8浏览器全部返回1。

    function a(){
        a = 1;
        console.log(a);
    };
    a();
    (function a(){
        a = 1;
        console.log(a);
    })();
    

    2.3 用途

    IIFE一般用于构造私有变量,避免全局污染。

    接下来,下面有一个更直观的需求来说明IIFE的用途。假设有一个需求,每次调用函数,都返回加1的一个数字(数字初始值为0)。

    1、全局变量

    一般情况下,我们会使用全局变量来保存该数字状态

    var a = 0;
    function add(){
        return ++a;
    }
    console.log(add());//1
    console.log(add());//2
    

    2、上面的方法局限在于变量a只和add函数有关,却声明为全局变量,不太合适。

    将变量a更改为函数的自定义属性更为恰当。

    function add(){
        return ++add.count;
    }
    add.count = 0;
    console.log(add());//1
    console.log(add());//2
    

    3、然后这样做可能还会存在问题,有些时代代码可能会无意间将add.count重置。使用IIFE把计数器变量保存为私有变量更安全,同时也减少全局变量的污染。

    var add = (function(){
        var counter = 0;
        return function(){
            return ++counter; 
        }
    })();
    console.log(add())//1
    console.log(add())//2 
    

    2.4 注意事项

    IIFE称为立即执行函数,这个立即执行函数有多立即呢?

    立即执行函数再快也得按照代码执行顺序去逐行执行。

    var a = 1;
    (function(){
        console.log(a);//1
    })();
    

    类似的,函数也是如此。

    function a(){
        return 1;
    }
    (function(){
        console.log(a());//1
    })();
    

    但,如果是函数表达式就不一样了。执行代码如下,会报错,提示a的值是undefined。

    var a = function(){
        return 1;
    }
    (function(){
        console.log(a());//报错
    })();
    

    函数有一个函数声明提前hoisting的过程,函数表达式其实分为先声明后赋值这两步。而,如果后者存在立即执行函数表达式,这个IIFE会快带函数表达式a执行完第一步函数声明后IIFE就会立即执行,此时a未被赋值,是undefined,所以执行a()时会报错。

    3、常见的一个循环和闭包错误的理解

    关于常见的一个循环和闭包的错误,很多资料都对此有文字解释,但还是难以理解,本文将以执行环境图示的方式来对此进行更直观的解释,以及对此需求进行推衍,得到合适的解决方法。

    3.1 犯错

    function foo(){
        var arr = [];
        for(var i = 0; i < 2; i++){
            arr[i] = function(){
                return i;
            }
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]());//2  
    

    以上代码的运行结果是2,而不是预想的0。接下来用执行环境图示的方法,详解到底是哪里出了问题。

    犯错执行流图示

    执行流首先创建并进入全局执行环境,进行声明提前的过程。执行流执行到第10行,创建并进入foo()函数执行环境,并进行其词法作用域内的声明提前。然后执行第2行,将arr赋值为[]。然后执行到第3行,给arr[0]和arr[1]都赋值一个匿名函数。然后执行到第8行,以arr的值为返回值退出函数。由于此时有闭包的存在,所以foo()的执行环境并不会被销毁,且i的值被保存。

    foo()执行环境未被销毁且i值保存

    执行流进入全局执行变量,继续执行第10行,将函数的返回值arr赋值给bar。

    foo()环境到全局执行环境

    执行流执行到第11行,访问bar的第0个元素并执行。此时,执行流创建并进入匿名函数执行环境,匿名函数中存在自由变量i,需要使用其作用域链匿名函数->foo()函数->全局作用域进行查找,最终在foo()函数的作用域找到了i,然后在foo()的执行环境找到了i的值为2,于是给赋值为2。

    执行流接着执行第5行,以i的值2作为返回值返回。同时销毁匿名函数的执行环境。执行流进入全局执行环境,接着执行第11行,调用内部对象console,并找到其方法log,将bar0的值2作用参数放入该方法中,最终在控制台显示2。

    由此我们看出,犯错的原因主要在循环的过程中,并没有把函数的返回值赋值给数组元素,而仅仅把函数赋值给了数组元素。这就使得在调用匿名函数时,通过作用域找到的执行环境中存储变量的值已经不是循环时的瞬时索引值,二十循环执行完毕之后的索引值。

    3.2 IIFE

    由此,可以利用IIFE传参和闭包来创建多个执行环境来保存循环时各个状态的索引值。因为函数传参是按值传递的,不同的参数的函数调用时,会创建不同的执行环境。

    function foo(){
        var arr = [];
        for(var i = 0; i < 2; i++){
            arr[i] = (function fn(j){
                return function test(){
                    return j;
                }
            })(i);
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]());//0  
    
    循环+IIFE

    3.3 块作用域

    使用IIFE还是较为复杂,使用作用域块则更为方便。

    由于块作用域可以将索引值i重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值,相当于每一次索引值都创建一个执行环境。

    function foo(){
        var arr = [];
        for(let i = 0; i < 2; i++){
            arr[i] = function(){
                return i;
            }
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]());//0    
    

    在编程中,如果实际和预期结果不符,就按照代码顺序一步一步地把执行环境图示画出来,会发现很多时候就是在想当然!

    4、闭包的7中形式

    根据闭包的定义,我们知道,无论通过各种手段,只要将内部函数传递到词法作用域以外,它都会持有对原有作用域的引用,无论在何处执行这个函数都会使用闭包,下面会介绍闭包的7种形式。

    4.1 返回值

    最常用的形式是函数作为函数值被返回:

    var F = function(){
        var b = 'local';
        var N = function(){
            return b;
        }
        return N;
    }
    console.log(F()());
    

    4.2 函数赋值

    一种变形的形式是将内部函数赋值给一个外部变量:

    var inner;
    var F = function(){
        var b = 'local';
        var N = function(){
            return b;
        };
        inner = N;
    };
    F();
    console.log(inner());
    

    4.3 函数参数

    闭包可以通过函数参数传递函数的形式来实现:

    var Inner = function(fn){
        console.log(fn());
    }
    var F = function(){
        var b = 'local';
        var N = function(){
            return b;
        }
        Inner(N);
    }
    F();
    

    4.4 IIFE

    前面的代码实例可知,函数F()都是在声明之后立即被调用,因此可以使用IIFE来代替。但是,需要注意的是,这里的Inner()只能用函数声明语句的形式,而不能用函数表达式。

    function Inner(fn){
        console.log(fn());
    }
    
    (function(){
        var b = 'local';
        var N = function(){
            return b;
        }
        Inner(N);
    })();
    

    4.5 循环赋值

    在闭包问题上,最常见的一个错误就是循环赋值的错误。其错误的原因在之前闭包环节已经讲述。

    错误:

    function foo(){
        var arr = [];
        for(var i = 0; i < 2; i++){
            arr[i] = function(){
                return i;
            }
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]());//2    
    

    正确:

    function foo(){
        var arr = [];
        for(var i = 0; i < 2; i++){
            arr[i] = (function fn(j){
                return function test(){
                    return j;
                }
            })(i);
        }
        return arr;
    }
    var bar = foo();
    console.log(bar[0]());//0    
    

    4.6 g(s)etter

    我们通过提供getter()函数和setter()函数老将要操作的变量保存在函数内部,防止其暴露在外部。

    var getValue,setValue;
    (function(){
        var secret = 0;
        getValue = function(){
            return secret;
        }
        setValue = function(v){
            if(typeof v === 'number'){
                secret = v;
            }
        }
    })();
    console.log(getValue());//0
    setValue(1);
    console.log(getValue());//1
    

    4.7 迭代器

    我们经常使用一个闭包来实现一个累加器。

    var add = (function(){
        var counter = 0;
        return function(){
            return ++counter; 
        }
    })();
    console.log(add())//1
    console.log(add())//2 
    

    类似地,使用闭包可以很方便的实现一个迭代器。

    function setup(x){
        var i = 0;
        return function(){
            return x[i++];
        }
    }
    var next = setup(['a','b','c']);
    console.log(next());//'a'
    console.log(next());//'b'
    console.log(next());//'c'
    

    全文博文地址:闭包

    相关文章

      网友评论

          本文标题:javascript之理解闭包

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