JavaScript初探之闭包

JavaScript中的变量作用域、作用域链与闭包。

一、变量作用域

  要理解闭包,首先要从作用域开始。
  ● 变量作用域的概念:就是一个变量可以使用的范围
  ● JS中首先有一个最外层的作用域:称之为全局作用域
  ● JS中还可以通过函数创建出一个独立的作用域,其中函数可以嵌套,所以作用域也可以嵌套

    // 多级作用域
    // --> 1级作用域
    var gender="男";
    function fn(){
        // gender: 可以访问
        // age: 可以访问
        // height: 不能访问

        // --> 2级作用域
        return function(){
            // gender: 可以访问
            // age: 可以访问
            // height:可以访问

            // --> 3级作用域
            var height=180;
        }
        var age=5;
    }

  注意:JS中变量的声明和赋值是在两个不同时期的。如下,当fn函数执行的时候,首先找到函数内部所有的变量、函数声明,把它们放在作用域中,给变量一个初始值:undefined,此时变量可以访问。之后才是逐条执行代码,在执行代码的过程中,如果有赋值语句,对变量进行赋值。

    function fn() {
        console.log(age);  // undefined
        var age = 18;
        console.log(age);  // 18
    }

二、作用域链

  由于作用域是相对于变量而言的,而如果存在多级作用域,这个变量又来自于哪里?这个问题就需要好好地探究一下了,我们把这个变量的查找过程称之为变量的作用域链。
  作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)
  简单来说,作用域链可以用以下几句话来概括:(或者说:确定一个变量来自于哪个作用域)
  Step1 查看当前作用域,如果当前作用域声明了这个变量,就确定结果
  Step2 查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明
  Step3 再查找上级函数的上级函数,直到全局作用域为止
  Step4 如果全局作用域中也没有,我们就认为这个变量未声明(抛出异常:xxx is not defined)
  举例:

    function fn(callback){
        var age=18;
        callback()
    }

    fn(function(){
        console.log(age);  // age is not defined
        // 分析:age变量:
        // 1.查找当前作用域:并没有
        // 2.查找上一级作用域:全局作用域
        // -->难点:看上一级作用域,不是看函数在哪里调用,而是看函数在哪里编写
        // -->因为这种特点,我们通常会把作用域说成是:词法作用域
    })

三、闭包

1. 闭包的概念

  关于闭包的概念,很多书和文档的描述都不一致。
  在红宝书中是这样认为的:如果有一个外层函数fn,它有一个内层匿名函数,那么此时可以把这个内层的匿名函数称为闭包函数。
  也就是,如果某个函数能够访问外层函数的变量,那么这个函数称为闭包函数。

  误区:必须return函数。
  闭包的概念里并没有规定是否要有return,但一般return才有意义。可以return一个函数,也可以return一个对象。

2. 闭包的问题

  举下面这个例子,思考三次执行f1()分别输出什么。

    function fn(){
        var a=5;
        return function(){
            a++;
            console.log(a);
        }
    }
    var f1=fn();  // f1指向匿名函数
    f1();  // 6
    f1();  // 7
    f1();  // 8

  三次执行f1()分别输出 6,7,8。下面分析下原因:
  代码执行到var f1=fn(),fn函数执行完毕,返回匿名函数。一般认为函数执行完毕,变量就会释放,但是此时由于js引擎发现匿名函数要使用a变量,所以a变量并不能得到释放,而是把a变量放在匿名函数可以访问到的地方去了。
  也就是,a变量存在于f1函数可以访问到的地方,当然此时a变了只能被f1函数访问。

3. 闭包问题的产生原因

  函数执行完毕后,作用域中保留了最新的a变量的值。

4. 闭包内存释放

    function f1(){
        var a = 5;
        return function(){
            a++;
            console.log(a);
        }
    }
    var q1 = f1();

    // 要想释放q1里面保存的a,只能通过释放q1
    q1 = null;    // 或者 q1 = undefined

5. 闭包的应用场景

  ● 模块化(利用闭包的原理,将一个大的系统放在一个自调用函数中)
  ● 防止变量被破坏

  举一个我在学习过程中看到的一个比较好的例子,代码和相关注释如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>闭包的应用</title>
</head>
<body>

</body>
<script>
    /*
    * 需求描述:
    * 有一个KTV,用户需要满足最低消费后才能进行支付(保护变量,将变量定义在函数内部)
    * 当来了一个身份特殊的朋友要来唱K时候,老板需要去修改最低消费的金额
    * 但是并不能让老板直接去修改leastPrice,或者说不能把leastPrice作为全局变量
    * 需要进行id验证,确认是老板后,才能修改leastPrice
    * */

    //模块化思想:也是一种设计模式
    var ktv = (function KTV() {
        //为了保护leastPrice变量,将它放在函数内部
        var leastPrice = 1000;
        var total = 0;

        return {
            // 购物
            buy: function (price) {
                total += price;
            },
            // 结账
            pay: function () {
                if (total < leastPrice) {
                    console.log('请继续购物');
                } else {
                    console.log('欢迎下次光临');
                }
            },
            editLeast: function (id, price) {
                if (id === 888) {
                    leastPrice = price;
                    console.log("现在最低消费金额为:", leastPrice);
                } else {
                    console.log('权限不足');
                }
            }
        }
    })();
</script>
</html>

四、总结

  只有理解了作用域链,才能很好地理解闭包。闭包的应用非常广泛,在JS中要实现模块化,最简单的就是使用闭包。未来在开发一些组件的时候,可以多使用闭包。
  例如下面两个组件(仅是举例):

    var datepicker=(function(){
        var hour=3600*1000;
        return function(){
            console.log('日期控件初始化');
        }
    })();

    var common=(function(){
        return {
            isStr:function(){

            },
            isNumber:function(){

            }
        }
    })();

  目录