首先,如果你想要了解JavaScript中闭包概念,和闭包执行过程中发生了什么,那么你必然要知道JS的执行环境和作用域链的概念。如果你说你不懂作用域链和执行环境,但是你能理解闭包,那么我认为你很厉害,至少我是没有搞明白!(在不了解作用域链和执行环境的情况下)。
注:本人新手一枚,说一下自己的见解,如果有什么地方不对,还请各位大牛批评指正。
执行环境:
执行环境是js中最为重要的一个概念。执行环境定义了变量和函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(varable obje),环境中定义了所有变量和函数都保存在这个对象中。(这里只是个概念,我们写的代码无法访问这个对象,而JS引擎在后台解析的时候回调用它)
VO(变量对象)
JS解释器如何找到我们定义的函数和变量的?它通过VO找到,VO是一个抽象概念中的“对象”(后台创建),它用于存储执行环境中的变量、函数声明、函数参数。
VO按照如下顺序填充数据:
1.函数参数(若未传入,初始化该参数值为undefined)
2.函数声明(若发生命名冲突,会覆盖前面的函数)
3.变量声明(初始化变量值为undefined,若发生命名冲突会被忽略。)
请看下列代码:
fn();
function fn(){
console.log(a);
}
function fn(d){
console.log(b);
console.log(a);
console.log(d);
console.log(c);
}
var a = 10;
var a = 20;
var b = 20;
c = 10;
上例代码是很常见的一个变量声明前置和函数声明前置,上述代码会打印undefined、undefined、undefined。当执行到console.log(c);会报错,因为该变量未定义。这里fn()之所以能在函数声明之前调用时因为js解析器会提前将这些变量赋值给VO对象前面已经说过了我们定义的变量是通过VO对象找到的。至于为什么值是undefined,是因为变量声明(初始化变量值为undefined,若发生命名冲突会被忽略。)注意看VO对象。
再看一个例子:
var age = 10;
sum();
onSum();
function sum(){
console.log(age);
var age = 20;
console.log(age);
console.log(onSum);
}
var onSum = function(){
console.log(age);
}
这里代码执行的结果是undefined、20、undefined、报错。先说下可能很多人都会认为第一行会打印10、而这里却打印了undefined、为什么呢。其实是因为函数有自己[[Scope]]内部属性(可以理解为指针),这个属性可以访问自己的作用域链,而作用域链中会包含自己的变量对象和外部的变量对象(在这里是全局变量对象)。但是sum函数的变量对象位于作用域的前端,当js引擎去查找的时候,找的了sum函数中定义的变量age所以就返回了结果。如果没有会一直向上查找,直到全局环境的变量对象返回。
全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在宿主环境不同,表示执行环境的对象也不一样。在Node.js中全局执行环境是global对象。而在浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境就会被销毁,保存在其中的素有变量和函数定义也随之被销毁(全局执行环境直到应用程序退出---例如关闭网页或浏览器,才会被销毁)。
每个函数都有自己的执行环境。当执行流进入到一个函数的时候(调用这个函数),函数的环境就会被推入一个环境栈中。而在函数执行之后,栈会将该环境弹出,并将控制权返回给之前的执行环境。ECMAScript程序中的执行流就是由这个机制控制着。
function fn1(){
function fn2(){
}
fn2();
}
fn1();
如上图所示:首先进入的是全局执行环境,当调用fn1()函数的时候,fn1()就会被推入环境栈中,而这时候控制权就在fn1()函数的手中,在fn1()中调用fn2()函数的时候,fn2()就会被推入环境栈中,这时候控制权就在fn2()手中。当fn2()执行完毕了,该执行环境就会被销毁,然后控制权就会被交给fn1()手中。当fn1()执行完毕之后,fn1()的执行环境就会被销毁,这时候就会将控制权交给全局对象。全局执行环境只在程序退出才会被销毁。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链(scope chain)的用途,是保证该执行环境能访问的所有变量和函数。作用域的前端,始终是当前执行的代码所在函数的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始的时候只包含一个变量,即arguments对象。作用域链中的下一个变量对象来自外部环境(包含它的函数,或者全局环境),一直延续到全局执行环境;全局执行环境的变量对象始终是作用域链的最后一个对象。
var globalVal = "is global";
function fn(){
var fnVal = "is fn";
function fn2(){
var fn2Val = 'is fn2';
}
fn2();
}
fn();
这个代码中全局执环境可以访问globalVal 、 fn() 。而fn()函数可以访问自己的变量fnVal、以及全局执行环境中的所有变量。最后fn2()函数可以访问自己的变量fn2Val以及外部的所有变量。看下列图片:
在后台的每个执行环境都有一个便是变量的对象--VO。全局环境的变量对象始终存在。而函数的局部环境中的变量对象,则只在该函数执行的过程中存在。在创建fn()函数的时候,会创建一个预先包含全局变量对象的作用域链,这个作用域链拜保存在内部的[[Scope]]属性中。当调用fn()函数的时候,会为该函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。接着又将该执行环境的变量对象推入作用域链的前端。
在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。如上图所示,如果在fn2()函数中打印globalVal变量,那么会首先查找自己的变量对象,没有,然后接着向上查找fn()函数中的变量,没有,再接着查找全局变量有,返回该变量。如果没有就会报错 说明这个变量未定义。
当在一个函数的内部定义的函数会将外部函数的变量对象添加到他的作用域链中。如上图所示:fn2执行环境会将fn执行环境的变量对象添加到他的作用域链中。
闭包:
闭包是指有权访问两一个函数作用域中变量的函数。创建闭包的方式就是函数中包含一个函数。
function sup(){
var a = 12;
var oj = { o : "o"};
return function(){
return {
name : "tony",
age : 24,
o : oj
}
}
}
var fn = sup();
var o = fn();
console.log(o.o.o); //"o"
在sup()函数调用的时会返回一个匿名函数,这个函数的执行环境中包含了自己的变量对象,sup的变量对象和全局执行环境的变量对象。当sup()函数执行完毕之后它的变量对象也不会拜销毁(js的垃圾回收机制是按标记回收的,而这时候sup的变量对象被它返回的匿名函数的作用域链所引用,所以不会被消除)。因为匿名函数的作用域链还在引用这个变量对象。
最后留一个题目给大家
查看console.log()输出的值,查看结果,思考为什么会是这样的。
function reder(){
var o = {
name : "oname",
age : 23
}
return function(){
var oj = { name : "oj", o : o};
return {
ge : function(){
return {
oj : oj,
o : o,
name : "xx"
}
}
};
};
}
var fn1 = reder();
var o1 = fn1();
var o2 = fn1();
var o11 = o1.ge();
var o22 = o2.ge();
console.log(o1.o === o2.o);
console.log(o1.ge === o2.ge);
console.log(o1.o === o2.o);
console.log(o11.o === o22.o);
console.log(o11.oj === o22.oj);