在描述闭包的实现与用途前,需要了解以下一些知识点。
执行上下文(执行上下文环境)
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上下文环境依旧在栈中没有销毁。
虽说闭包的使用增加了内存的消耗,而且使用不当有可能造成内存泄露的问题发生,不过其还是有很多的用途。譬如匿名自执行函数、匿名自执行函数、实现封装以及实现面向对象中的对象,模拟传统的对象语言都提供类的模板机制等等。