(第四天)作用域链、闭包
前言
JavaScript是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。全局变量在程序中始终都是有定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。
如果将一个局部变量看做是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段JavaScript代码(全局代码或函数)都有一个与之关联的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当JavaScript需要查找变量的时候(这个过程称做“变量解析”),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此内推。如果作用域链上没有任何一个对象含有属性x,那么将认为这段代码的作用域链上不存在x,并最终抛出一个引用错误。
在JavaScript的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域上至少有三个对象。理解作用域链的概念对于理解with语句以及理解闭包的概念至关重要。
作用域链
下面定义一个person类来进行作用域链举例说明。
1 var global = "global"; 2 function person(){ 3 var age = 12; 4 var name = "小黑"; 5 return function(){ 6 console.log("年龄为:"+age+","+"名字为:"+","+name); 7 } 8 } 9 var p = person(); 10 p();
通过上述代码来分析其作用域链:
(1)当使用函数声明语句定义person函数时就产生了一个作用域链,第一个存的是person的地址,另一个是全局变量global的地址,并保存了这个作用域链。
(2)调用函数person时,此时创建一个函数person的变量对象来存储age和name变量以及另外一个匿名函数,同时将该变量对象添加到上述(1)中保存的作用域链上,此时(1)中的person的地址就指向了创建的变量对象。此时将从作用域链由上至下查找,从作用域链中的第一个变量对象开始查找,找到变量age和name并为其赋值为12和小黑。接下来再创建一个全局变量对象即(window变量对象)并且其保存作用域链地址指向该变量对象,同理存放变量global以及它的值。
(3)当调用函数p时,此时将有三个地址即指向函数p变量对象的地址,函数person变量对象的地址,global全局变量对象的地址。p变量对象中没有变量为空对象,此时如(2)一样将变量对象又被重新创建一遍。
(4)可以对照下面作用域链图来看上述解释【注】基本上是个人看JavaScript权威指南加上理解所画,若有园友觉得不妥或解释错误请指出,以供我继续学习。
函数调用作用域链变化
当定义一个函数时,它实际上保存了一个作用域链。当调用这个函数时,它创建了一个新的对象来存储它的局部变量,并将这个对象添加到保存的作用域链上,同时创建一个新的更长的表示函数调用作用域的链。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次 调用外部函数的时候,作用域链都是不同的。
闭包
定义
函数定义时的作用域链到函数执行时依然有效。
引入
在理解闭包之前,先看看在作用域链中对局部变量是怎样进行处理的。我们将作用域链描述为一个对象列表,不是绑定的栈。当调用JavaScript函数的时候,都会为之创建一个新的对象用来保存局部变量,并把这个对象添加至作用域链中。当函数返回的时候,就从作用域链中将这个绑定变量的对象删除,如果不存在嵌套的函数,也没有其他引用指向这个绑定对象,它就会被当做垃圾回收掉。如果定义了嵌套函数,每个嵌套的函数都各自对应一个作用域链,并且这个作用域链指向一个变量绑定对象。但如果这些嵌套的函数对象在外部函数中保存下来,那么它们也会和所指的变量绑定对象一样当做垃圾回收。但如果这个函数定义了嵌套函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它将不会被当做垃圾回收,并且它所指向的变量绑定 对象也不会被当做垃圾回收。我们要明确的了解闭包和垃圾回收之间的关系,如果使用不慎,闭包很容易造成【循环引用】,当在DOM对象和JavaScript对象之间存在循环引用时需格外小心,在某些浏览器中会造成内存泄露。
讨论
我们用代码来解释所谓的闭包
1 var scope = "global scope"; 2 function checkscope() { 3 var scope = "local scope"; 4 function f() { return scope; } 5 return f(); 6 } 7 cosole.log(checkscope());
根据上述代码可以打印出 local scope ,如果不知道为什么打印出这个结果,请参看前面文章。在函数checkscope中定义了变量 scope ,但返回函数f时应该被销毁才是,通过上面作用域链知,在返回的函数f的作用域链中有存了函数checkscope的变量对象,并且函数f中引用了其变量对象中的值,所以此时变量scope不会当做垃圾回收并销毁。所以闭包的含义浅显的说就是: 指能够访问外部函数作用域中变量的函数 ,更加通俗一点讲就是:
1 假如有一个函数A和函数B,如果现在B函数访问了函数A中的局部变量,那么此时函数B就叫做闭包。
下面我们将上述代码进行小小的改动
1 var scope = "global scope"; 2 function checkscope() { 3 var scope = "local scope"; 4 function f() { return scope; } 5 return f; 6 } 7 cosole.log(checkscope()());
在函数checkscope中最后返回的是函数对象,然后在定义函数的作用域外面,调用这个嵌套的函数,此时应该其局部变量 scope 是不是销毁了,所以打印出 global scope 呢!答案是NO,我们知道作用域链在函数定义的时候就创建了,嵌套的函数f()定义在这个作用域链里,其中的scope一定是局部变量,所以不管在何时何地执行函数f(),这种绑定在执行函数f()时依然有效,所以还是打印出 local scope。 通过此我们知道闭包的特性是如此的强大,强大到可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。
同一作用域链共享状态(变量或私有变量)
下面用代码说明在同一作用域链中共享变量
1 fcuntion counter(){ 2 var n = 0; 3 return { 4 count : function(){ return n++ ;}, 5 reset : function(){ return n = 0;} 6 }; 7 8 } 9 10 var c = counter(), d = counter(); /*创建两个计数器*/ 11 c.count(); // =>0 12 d.count(); // =>0:它们互不干扰 13 c.reset(); //reset()和count() 方法共享状态 14 c.count(); // =>0:因为重置了c 15 d.count(); // =>1: 而没有重置d
首先要理解,上述count()和reset()这两个方法都可以访问私有变量n,再者每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或者reset()不会影响另外一个对象。
【注意】不希望共享的变量却共享其他的闭包
请看下面代码
1 function constfuncs() { 2 var funcs = []; 3 for (var i = 0; i < 10; i++) 4 funcs[i] = function() { 5 return i; 6 } 7 return funcs; 8 } 9 var funcs = constfuncs(); 10 console.log(funcs[5]());
上面代码创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值为10,所有的闭包都共享这一个值,因此数组中的函数返回值都是一个值,所以结果打印出 10 。这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的编程生成静态快照。书写闭包写的时候,注意this是一个关键字而不是变量。要想使闭包在外部函数里访问this,可以使外部函数将this转存为一个变量: var self = this; 同理arguments也是关键字也需保存起来以便嵌套的函数能使用它如 var outerArguments = arguments;
上述代码改写为如下即可
1 //这个函数返回一个总是v的函数 2 function constfunc(v) { return function() { return v; }; } 3 4 //创建一个数组用来存储常数函数 5 var funcs = []; 6 for(var i =0; i < 10; i++) funcs[i] = constfunc(i); 7 8 //在第五个位置的元素所表示的函数 返回值为5 9 console.log(func[5]()); // =>5
或者通过立即执行的匿名函数进行改写
1 function constfunc(){ 2 var funcs = []; 3 for(var i = 0; i < 10; i++){ 4 funcs[i] = (function (n){ 5 return n; 6 })(i); 7 } 8 return funcs; 9 } 10 11 var funcs = constfunc(); 12 console.log(funcs[5]); // =>5