【javascript】Javascript闭包

限于喜欢 提交于 2020-02-01 01:58:33

  在描述闭包的实现与用途前,需要了解以下一些知识点。



  执行上下文(执行上下文环境) 


 

1 console.log(a);     //Uncaught ReferenceError: a is not defined 
2 
3 console.log(b);    //undefined
4 var b;
5 
6 console.log(c);    //undefined
7 var c = 10;

  看上面一个例子,当直接在控制台输出a时,提示a未定义,这个很容易就理解了。不过在"var b"和"var c = 10"执行前输出b和c的值时,得到的结果却是undefined。因为代码是一条一条执行的,在执行console.log()之前,"var b"和"var c = 10"必定是还未执行的。而结果却与a的输出结果不同,这是怎么一回事?

 第二种情况:

 1 console.log(this); 

输出:

第三种情况:

1 console.log(f1);  
2 function f1(){};  //函数声明
3 
4 console.log(f2);
5 var f2 = function (){};  //函数表达式

输出: 

  从这里可以看出来,在一段js代码拿过来真正一句一句运行之前,浏览器做了一些“准备工作”,其中就包括对变量的声明,而不是赋值,变量赋值是在赋值语句执行的时候进行的,还有函数的声明的赋值。而函数表达式类似与第一种情况中的var c = 10,两者结果也是相同的。当然还有this的赋值等等。

  下面总结一下浏览器在准备中做了哪些工作:

    变量、函数表达式 —— 变量声明,默认赋值为undefined;

    this —— 赋值 

    函数声明 —— 赋值

  这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。

  以上的一切都是在全局环境中进行的。

  在函数中,除了以上数据之外,还会有其他数据。

1 function f(x){
2     console.log(arguments);
3     console.log(x);
4 }
5 f(10);

输出:

  以上代码展示了在函数体的语句执行之前,arguments变量和函数的参数都已经被赋值。

  从而我们可以看出来,函数每被调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就会有不同的参数。

  进一步来说,函数在定义的时候(不是调用的时候),就已经确定了函数体内部自由变量的作用域。

总结一下:

  全局代码的上下文环境数据内容为:

普通变量(包括函数表达式),

如: var a = 10;

声明(默认赋值为undefined)

函数声明,

如: function fn() { }

赋值

this

赋值

  如果代码段是函数体,那么在此基础上需要附加:

参数

赋值

arguments

赋值

自由变量的取值作用域

赋值

 

  给执行上下文环境下一个通俗的定义——在执行代码之前,把将要用到的所有的变量都事先拿出来,有的直接赋值了,有的先用undefined占个空。



  在执行js代码时,会有数不清的函数调用次数,会产生许多个上下文环境。这么多上下文环境该如何管理,以及如何销毁而释放内存呢?这就引入了一个概念——执行上下文栈


  在这里插播一个概念 —— this

  在平时,this使用频率非常高,我们在很多情况下都会用到它。而无论在哪个位置获取this,都是有值的。在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了。因为this的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

  接下来,分四种情况描述this的取值:

  情况1:构造函数

    所谓构造函数就是用来new对象的函数。其实严格来说,所有的函数都可以new一个对象,但是有些函数的定义是为了new一个对象,而有些函数则不是。另外注意,构造函数的函数名第一个字母大写(规则约定)。例如:Object、Array、Function等。

    如果函数作为构造函数用,那么其中的this就代表它即将new出来的对象。这里有一个情况,当函数的调用不是new一个对象,而是直接调用函数时,其中的this的值为window。

    这里还有一个问题,在构造函数的prototype中,this代表着什么。其实不仅仅是构造函数的prototype,即便是在整个原型链中,this代表的也都是当前对象的值。

  情况2:函数作为对象的一个属性

    如果函数作为对象的一个属性时,并且作为对象的一个属性被调用时,函数中的this指向该对象。

  情况3:函数用call或者apply调用

    当一个函数被call和apply调用时,this的值就取传入的对象的值。 

  情况4:全局 & 调用普通函数

    在全局环境下,this永远是window,普通函数在调用时,其中的this也都是window。


 下面继续学习上下文栈。

  执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。这其实就是一个压栈出栈的过程——执行上下文栈。如图:

  在上下文栈中,同一时间内只有一个活跃的上下文环境。

  当一个函数被调用时,将产生一个新的执行上下文环境,并被压入执行上下文栈中,原先的栈顶执行上下文环境进入非活跃状态,新压入的执行上下文环境则进入活动状态。

  当函数调用完成后,栈顶执行上下文环境被弹出栈,并被回收机制回收销毁,而新的栈顶上下文环境则进入活跃状态。

  不过这只是一个理想的运行情况,有时候执行上下文环境并不会说销毁就会被销毁的,这就是后面要介绍的——闭包。



   作用域 & 作用域链


  无论在哪个编程语言中,都有一个作用域的概念。而在javascript中,除了全局作用域之外,只有函数可以创建的作用域。作用域在函数定义时就已经确定了。而不是在函数调用时确定。

  所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。因为在函数中,如果声明变量时没用"var",则所声明的变量为全局变量。

  作用域是一个很抽象的概念,类似于一个“地盘”,如下图:

  作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。

  作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

  我们把作用域与上下文环境结合起来看,如下图:

  

  在上图中,我们可以看到,作用域只是一个“地盘”,一个抽象的概念,其中没有变量。要通过作用域对应的执行上下文环境来获取变量的值。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。所以,作用域中变量的值是在执行过程中产生的确定的,而作用域却是在函数创建时就确定了。

  所以,如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。

  这里有一个概念——自由变量。所谓"自由变量",即在这一个作用域中调用的变量,并没有在这个作用域中声明,而是在其他作用域声明的变量,即称之为"自由变量"。

  而我们在这个作用域中调用自由变量时,究竟要到哪里去获取这个变量的值呢?有人会说,到这个作用域的父作用域调用。这个说法有点模糊,有时候会产生歧义。正确来说,应该是:要到创建这个函数的那个作用域中取值——是“创建”,而不是“调用”。其实这也就是所谓的"静态作用域"。

  上面描述的只是跨一步作用域去寻找。

  如果跨了一步,还没找到呢?——接着跨!——一直跨到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。

  这个一步一步“跨”的路线,我们称之为——作用域链。

  下面我们来用文字描述一下这个过程:(假设a为自由变量)  

    第一步,现在当前作用域查找a,如果有则获取并结束。如果没有则继续;

    第二步,如果当前作用域是全局作用域,则证明a未定义,结束;否则继续;

    第三步,(不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;

    第四步,跳转到第一步。



  接下来就是主菜 —— 闭包


    关于闭包,百度的解释是:闭包是可以包含自由(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。。。看起来有点云里雾里的。

  其实,我们只要了解应用的两种情况即可——函数作为返回值,函数作为参数传递。

  函数作为返回值:

 1 function fn(){
 2     var a = 10;
 3     
 4     function bar(x){
 5         if (X < a){
 6             console.log(X);
 7         }
 8     }
 9     
10     return bar;
11 }

  函数作为参数被传递:

 1 var 
 2     max = 10;
 3     fn = function(X){
 4          if (X > max){
 5              console.log(X);
 6          }
 7          else{
 8              console.log(max);
 9          }
10      }; 
11 
12 (function(f) {      
13      var max = 100;
14      f(15);
15  })(fn);

  这里的输出结果为: 15。原因为取max的值时取的是创建这个函数的那个作用域中获取,而不是“父作用域”。所以取的值为10,而不是100。

  结合前面学过的,我们可以看看下面这张图:

  在图中我们可以看到,执行完第17行,fn()调用完成。按理说应该销毁掉fn()的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行fn()时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量max要引用fn作用域下的fn()上下文环境中的max。因此,这个max不能被销毁,销毁了之后bar函数中的max就找不到值了。因此,这里的fn()上下文环境不能被销毁,还依然存在与执行上下文栈中。

  执行到第20行,执行f1(15),即执行bar(15),创建bar(15)上下文环境,并将其设置为活动状态。

  执行bar(15)时,max是自由变量,需要向创建bar函数的作用域中查找,找到了max的值为10。而重点在于,创建bar函数是在执行fn()时创建的。fn()早就执行结束了,但是fn()执行上下文环境还存在与栈中,因此bar(15)时,max可以查找到。如果fn()上下文环境销毁了,那么max就找不到了。从而fn上下文环境依旧在栈中没有销毁。

  虽说闭包的使用增加了内存的消耗,而且使用不当有可能造成内存泄露的问题发生,不过其还是有很多的用途。譬如匿名自执行函数、匿名自执行函数、实现封装以及实现面向对象中的对象,模拟传统的对象语言都提供类的模板机制等等。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!