js面试题之闭包

一、什么是闭包?

前言

百度百科定义:闭包就是能够读取其他函数内部变量的函数。
创建闭包的通常方式,是在一个函数内部创建另一个函数

在解释之前,得先讲讲作用域。先来看下面这个示例:

var a = 1;
function f(){
  var b = 2;
  console.log(a) // 1
}
console.log(b) // undefined

示例中包含了两种作用域,一种是属于全局的全局作用域,另一种是属于函数f的局部作用域。由于Javascript这种链式作用域(父作用域是可以被其子作用域访问的,而子作用域却不能被父作用域访问)的机制,使得示例最后一行输出了undefined

从此可以看出,无法从父作用域中访问子作用域。而我们再来看闭包的定义:闭包就是能够读取其他函数内部变量的函数。也就是闭包可以让我们从父作用域中访问到子作用域,具体怎么实现的呢?来看这个经典的例子

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

var baz = foo();

baz(); // 2 -> 这就是闭包的效果

这个示例中,闭包就是函数bar。可以看到,我们通过在函数foo内部定义其子函数bar,并将其作为foo返回值,因为bar函数作用域可以访问foo的作用域,所以实现了从全局作用域访问foo函数作用域的效果。

有了前面知识的充分准备
我们正是进入讲解闭包

想要真正理解闭包,需要理解 执行环境和活动对象

  • 执行环境和活动对象
  • 执行环境(execution context)定义了变量或者函数有权访问的其他数据,每个执行环境都有一个与之关联的变量对象(variable object),执行环境中定义的变量和函数就保存在这个变量对象中;
    全局执行环境是最外围的一个执行环境,通常被认为是window对象
    执行环境和变量对象在运行函数时生成
    执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)

什么垃圾文章,取关取关!”,我猜,看到这里,是不是觉得以上的理论很枯燥而且艰涩?因为基本上是从书上引用来的,不着急着理解,先摆在上面,等会结合案例回头再来看!相信你会恍然大悟,接下来请看样例:


<script>
    var a = 2;
    function A(){
         var a = 1;
         return a ;
    }
    console.log(A());//1
</script>

在执行函数A的时候,创建了A的执行环境和变量对象,其中A的变量对象和全局变量对象中都含有a变量,根据作用域链从前向后查找,在A的变量对象中找到,所以输出1,执行完毕以后 ,A的执行环境销毁,A的变量对象由于没有被引用,所以也销毁;

上文我们提到了,由于作用域链的结构,外围函数是无法访问内部变量的,为了能够访问内部变量,我们就可以使用闭包,闭包的本质还是函数。 也许你应该默认三遍 这句话 **闭包的本质还是函数
**,因为它实在是太重要了


<script>
    function A(){
        var x = 1;
        return function(){
            console.log(x);
        }
    }
    var m = A();
    m();//1
</script>

上面就是一个很简单的闭包例子,通过m函数,我们可以获得A函数内部变量的值,这个样例比较简单,看不出什么问题,接下来我们来深入了解一下。


<script>
    function A(){
        var x = 1;
        return function(){
            x++;
            console.log(x);
        }
    }
    var m1 = A();//第一次执行A函数
    m1();//2
    m1();//3
    var m2 = A();//第二次执行A函数
    m2();//2
    m1();//4
</script>

每次执行A函数时,都会生成一个A的活动变量和执行环境,执行完毕以后,A的执行环境销毁,但是活动对象由于被闭包函数引用,所以仍然保留,所以,最终剩下两个A的变量对象,因此m1和m2在操作x时,指向的是不同的数据,

现在来回答三个问题:
1.(为什么连续执行m1的时候,x的值在递增?)
answer:因为m1在引用的活动对象A一直没有释放(想释放的话可以让m1=null),所以x的值一直递增。
2.定义函数m2的时候,为什么x的值重新从1开始了?
answer:因为又一次运行了A函数,生成一个新的A的活动对象,所以m2的作用域链引用的是一个新的x值。
3.m1和m2里面的x为什么是相互独立,各自维持的?
answer:因为在定义m1和m2的时候,分别运行了A函数,生成了两个活动对象,所以,m1和m2的作用域链是指向不同的A的活动对象的。

好的,到这里先回顾一下前面说到的知识点:

执行环境和变量对象在运行函数时生成
执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)

感觉理解了吗?接下来,再看看另一个很类似的例子:


 <script>
    function A(){
        var x = 1;
        var m=[];
        m[0] = function(){
            x++;
            console.log(x);
        };
        m[1] = function(){
            x++;
            console.log(x);
        }
         return m;
    }
    var m = A();//第一次运行A,而且只运行这一次
    m[0]();//2
    m[1]();//3
    m[0]();//4
    m[1]();//5
</script>

这个例子和刚刚十分类似,不同的是,在A内部就先定义了两个函数,可以看出 ,最后的结果与上面的例子有些不同:
变量x仍然能保持递增,但是m[0]和m[1]定义的函数,对于x的改变不再是相互独立的,其实大家估计猜到了,这里的m[0]和m[1]的作用域指向的A的变量对象,其实是同一个,为什么呢?很简单,看看刚刚这段代码,其实是只调用了一次A函数,再看上文那句话:

执行环境变量对象在运行函数时生成

既然A只执行一次,那么A的活动变量当然也就生成了一个,所以这里m[0]和m[1]的作用域指向同一个A的变量对象


 <script>
    function A(){
        var funs=[];
        for(var i=0;i<10;i++){
           funs[i]=function(){
               return i;
           }
        }
        return funs; 
    }
    var funs = A();//定义funs[0]-funs[9],10个函数
    console.log(funs[0]());//10
    console.log(funs[1]());//10
    console.log(funs[6]());//10
</script>

这个例子其实算是一个经典案例,在很多地方都有提到,执行完毕后 funs数组中,funs[0]-funs[9]存的其实都是一样的,都是一个返回i值的函数,这个例子容易错误的地方其实在于,弄错了产生执行环境的时机,还是看这句话:

执行环境和变量对象在运行函数时生成

所以,当执行 var funs = A();时,只是定义函数,而没有执行,真正产生环境变量的时间是在console.log(funs0);这三句的时候,此时A的变量对象中i值是什么呢?很简单,看它return的时候,i的值,显然,i的值是10,所以,最后三句输出的都是10

二、闭包的作用

1.模仿块级作用域

首先简单举个例子来,解释一下什么是块级作用域:

function A(num) {
    for (var i = 0; i < num; i++) {
      num++;
    }
    console.log(i)
  }

在这个简单的函数中,变量i是在for循环中定义的,如果是在C++或者Java中,这样定义的变量,一旦循环结束,变量也就随之销毁,i的作用范围只在循环这个小块,就称为块级作用域。在javascript中,没有这样的块级作用域,前面一篇文章已经提到,变量是定义在函数的活动对象中的,因此,从定义i开始,在函数内部可以随时访问它。
这样的坏处显而易见:由于javascript不会告诉你变量是否已经被声明,容易造成命名冲突,如果是在全局环境定义的变量,就会污染全局环境,因此可以利用闭包特性来模拟块级作用域。不过在此之前要先介绍另一个知识点:匿名立即执行函数。如果已经比较熟悉的同学可以直接跳过这一块:

匿名立即执行函数
首先举个例子(我比较喜欢举例,感觉看例子比较更容易理解):

var helloWorld = function(){
    alert('Hello world')
}
helloWorld();//执行函数

上面的简短代码一共就做两件事:1.定义了一个匿名函数并赋值给helloWorld;2.在helloWorld后面加括号表示调用函数,所以 匿名函数如果直接执行,是不是应该这样写:

function(){
    alert('Hello world')
}()

这样的写法会报错,因为在javascript中,function是函数声明的标志,不允许在后面直接加括号,而应该写成这样:

(function(){
    //函数体
    alert('Hello world')
})()

也就是把声明部分加括号即可,加了括号以后,这一段代码就相当于执行了里面的函数体部分,但是此时内部的变量已经不能被外部访问

具体实现
现在我们讲模拟块级作用域的具体步骤,假设还是针对前面的A函数,如果我们想让i变量只有块级作用域,可以这样写:

function A(num) {
    //核心代码
   (funnction(){
    for(var i = 0; i<num; i++) {
      num++;
    }
    })()
    //核心代码结束
    console.log(i)//underfined
  }

注意看核心代码部分,我们用刚刚讲到的匿名自执行函数在内部形成了一个闭包,这个闭包在哪呢?一直强调,闭包的本质是函数,其实在这里闭包就是那个匿名函数,这个闭包可以到函数A内部的活动变量,又能保证自己内部的变量在自执行后直接销毁,这个应该不难理解了

2.存储变量

我们知道闭包的另一个特点是可以保存外部函数的变量,原理是基于javascript中函数作用域链的特点,内部函数保留了对外部函数的活动变量的引用,所以变量不会被释放(这一块没有理解清楚的请看前一篇文章,里面讲的比较详细),然后我们再来愉快地举例子:

function B(){
    var x = 100;
    return {
        function(){
            return x
        }
    }
}
var m = B()//运行B函数,生成活动变量 x被m引用

这是前文介绍过的一个最简单的闭包例子,我们运行B函数,返回值就是B内部的匿名函数,此时m引用了变量x,所以B执行后x不会被释放,利用这一点,我们可以把比较重要或者计算耗费很大的值存在x中,只需要第一次计算赋值后,就可以通过m函数引用x的值,不必重复计算,同时也不容易被修改。

3.封装私有变量
javascript中没有私有成员的概念,我们可以把函数当做一个范围,函数内的变量就是私有变量,在外部无法引用,比如:

function Person(){
    var name = 'default';
    this.getName:function(){
        return name;
    }
    this.setName:function(value){
        name = value;
    }
}
console.log(Person.getName())//default
console.log(Person.setName('mike'))
console.log(Person.getName())//mike

在这个例子中,设置了两个闭包函数来操作Person函数内部的name变量,除了这两个函数,在外部无法再访问到name变量,name也就相当于是私有成员。在这个例子中,我们用的是在构造函数中定义公有方法,对于所有的Person实例,都分别创建了新的办法,当然还可以使用其他形式来避免这个问题,要涉及到创建对象模式的一些知识

三、总结

简而言之,只有三句话

  1. 创建闭包的通常方式,是在一个函数内部创建另一个函数
  2. 闭包的本质还是函数
  3. 执行环境和变量对象在运行函数时生成,执行环境中的所有代码执行完以后,执行环境被销毁,保存在其中的变量和函数也随之销毁;(全局执行环境到应用退出时销毁)。

理解了者三句话,相信,你也已经学会了 闭包。

  • 13
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值