JavaScript从作用域到闭包
目录
作用域(scope)
全局作用域和局部作用域
通常来讲这块是全局变量与局部变量的区分。 参考引文:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链
全局作用域:最外层函数和在最外层函数外面定义的变量拥有全局作用域。
1)最外层函数和在最外层函数外面定义的变量拥有全局作用域
2)所有末定义直接赋值的变量自动声明为拥有全局作用域,即没有用var声明的变量都是全局变量,而且是顶层对象的属性。
3)所有window对象的属性拥有全局作用域
局部作用域:和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方也会看到有人把这种作用域称为函数作用域。
代码部分请参照引文。
块作用域与函数作用域
函数作用域是相对块作用域来进行解释的,其和局部作用域是一个意思。参考引文:JavaScript的作用域和块级作用域概念理解
块作用域:任何一对花括号{}中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是无效的,我们称之为块级作用域。
函数作用域:在函数中的参数和变量在函数外部是无法访问的。JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。下文会解释。
1 //C语言 2 #include <stdio.h> 3 void main() 4 { 5 int i=2; 6 i--; 7 if(i) 8 { 9 int j=3; 10 } 11 printf("%d/n",j); 12 }
运行这段代码,会出现“use an undefined variable:j”的错误。可以看到,C语言拥有块级作用域,因为j是在if的语句块中定义的,因此,它在块外是无法访问的。
1 function test(){ 2 for(var i=0;i<3;i++){}; 3 alert(i); 4 } 5 test();
运行这段代码,弹出"3",可见,在块外,块中定义的变量i仍然是可以访问的。也就是说,JS并不支持块级作用域,它只支持函数作用域,而且在一个函数中的任何位置定义的变量在该函数中的任何地方都是可见的。
作用域中的声明提前
var scope="global"; //全局变量 function t(){ console.log(scope); var scope="local" ;//局部变量 console.log(scope); } t();
(console.log()是控制台的调试工具,chrome叫检查,有的浏览器叫审查元素,alert()弹窗会破坏页面效果)
第一句输出的是: "undefined",而不是 "global"
第二讲输出的是:"local"
第二个不用说,就是局部变量输出"local"。第一个之所以也是"local",是因为Js中的声明提前,尽管在第4行才进行局部变量的声明与赋值,但其实是将第4行的声明提前了,放在了函数体顶部,然后在第4行进行局部变量的赋值。可以理解为下面这样。
var scope="global";//全局变量 function t(){ var scope;//局部变量声明 console.log(scope); scope="local";//局部变量赋值 console.log(scope); } t();
具体细节可以查阅犀牛书(《JavaScript权威指南》)中的详细介绍。
作用域链(Scope Chain)
当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。参考引文:Js作用域与作用域链详解,浅析作用域链–JS基础核心之一
num="one"; var a = 1; function t(){ //t函数的局部作用域,可以访问到a,b变量,但是访问不到c变量 var num="two"; var b = 2; function A(){ //A函数局部作用域,可以访问到a,b,c变量 var num="three"; //局部变量与外部变量重名以局部变量为主 var c = 3; console.log(num); //three } function B(){ //B函数局部作用域,可以访问到a,b变量,访问不到c变量 console.log(num); //two } A(); B(); } t();
当执行A时,将创建函数A的执行环境(调用对象),并将该对象置于链表开头,然后将函数t的调用对象链接在之后,最后是全局对象。然后从链表开头寻找变量num。
即:A()->t()->window,所以num是”three";
但执行B()时,作用域链是: B()->t()->window,所以num是”two";
另外,有一个特殊的例子我觉得应该发一下。利用“JavaScript 的作用域是词法性质的(lexically scoped)。这意味着,函数运行在定义它的作用域中,而不是在调用它的作用域中。” 这句话,解释了下面的例子。
var x = 10; function a() { console.log(x); } function b () { var x = 5; a(); } b();//输出为10
虽然b函数调用了a,但是a定义在全局作用域下,同样也是运行在全局作用域下的,所以其内部的变量x,向上寻找到了全局变量x=10;所以b函数的输出为10;
更深层次的讲解请参照:JavaScript 开发进阶:理解 JavaScript 作用域和作用域链。
经典案例
下面是一个经典的事件绑定例子:
<div id = "test"> <p>栏目1</p> <p>栏目2</p> <p>栏目3</p> <p>栏目4</p> </div> </body> <script type="text/javascript"> function bindClick(){ var allP = document.getElementById("test").getElementsByTagName("p"), i=0, len = allP.length; for( ;i<len;i++){ allP[i].onclick = function(){ alert("you click the "+i+" P tag!");//you click the 4 P tag! } } } bindClick();//运行函数,绑定点击事件 </script>
上面的代码给P标签添加点击事件,但是不管我们点击哪一个p
标签,我们获取到的结果都是“you click the 4 P tag!”。
我们可以把上述的JS
代码给分解一下,让我们看起来更容易理解,如下所示。前面使用一个匿名函数作为click
事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。
function bindClick(){ var allP = document.getElementById("test").getElementsByTagName("p"), i=0, len = allP.length; for( ;i<len;i++){ allP[i].onclick = AlertP; } function AlertP(){ alert("you click the "+i+" P tag!"); } } bindClick();//运行函数,绑定点击事件
这里应该没有什么问题吧,前面使用一个匿名函数作为click
事件的回调函数,这里使用的一个非匿名函数,作为回调,完全相同的效果。也可以做下测试哦。
理解上面的说法了,那么就可以很简单的理解,为什么我们之前的代码,会得到一个相同的结果了。首先看一下for
循环中,这里我们只是对每一个匹配的元素添加了一个click
的回调函数,并且回调函数都是AlertP
函数。这里当为每一个元素添加成功click
之后,i
的值,就变成了匹配元素的个数,也就是i=len
,而当我们触发这个事件时,也就是当我们点击相应的元素时,我们期待的是,提示出我们点击的元素是排列在第几行。当click
事件触发时,执行回调函数AlertP
,但是当执行到这里的时候,发现alert
方法中,有一个变量是未知的,并且在AlertP
的局部作用域中,也没有查找到相应的变量,那么按照作用域链的查找方式,就会向父级作用域去查找,这里的父级作用域中,确实是有变量i
的,而i的值,却是经过for
循环之后的值,i=len
。所以也就出现了我们最初看到的效果。
解决办法如下所示:
function bindClick(){ var allP = document.getElementById("test").getElementsByTagName("p"),
i=0,
len = allP.length; for( ;i<len;i++){ AlertP(allP[i],i); } function AlertP(obj,i){ obj.onclick = function(){ alert("you click the "+i+" P tag!"); } } } bindClick();
这里,obj
和i
在AlertP
函数内部,就是局部变量了。click
事件的回调函数,虽然依旧没有变量i
的值,但是其父作用域AlertP
的内部,却是有的,所以能正常的显示了,这里AlertP
我放在了bindClick
的内部,只是因为这样可以减少必要的全局函数,放到全局也不影响的。
这里是添加了一个函数进行绑定,如果我不想添加函数呢,当然也可以实现了,这里就要说到自执行函数了。可以跳到本文的自执行函数,也可以看参考引文的深度讲解:浅析作用域链–JS基础核心之一
函数声明与赋值
声明式函数、赋值式函数与匿名函数
匿名函数:function () {}; 使用function关键字声明一个函数,但未给函数命名,所以叫匿名函数,匿名函数有很多作用,赋予一个变量则创建函数,赋予一个事件则成为事件处理程序或创建闭包等等。下文会讲到。
JS中的函数定义分为两种:声明式函数与赋值式函数。
<script type="text/javascript"> Fn(); //执行结果:"执行了声明式函数",在预编译期声明函数及被处理了,所以即使Fn()调用函数放在声明函数前也能执行。 function Fn(){ //声明式函数 alert("执行了声明式函数"); } </script>
<script type="text/javascript"> Fn(); //执行结果:"Fn is not a function" var Fn = function(){ //赋值式函数 alert("执行了赋值式函数"); } </script>
JS的解析过程分为两个阶段:预编译期(预处理)与执行期。
预编译期JS会对本代码块中的所有声明的变量和函数进行处理(类似与C语言的编译),此时处理函数的只是声明式函数,而且变量也只是进行了声明(声明提前)但未进行初始化以及赋值。所以才会出现上面两种情况。
当正常情况,函数调用在声明之后,同名函数会覆盖前者。
<script type="text/javascript"> function Fn(){ //声明式函数 alert("执行了声明式函数"); } var Fn = function(){ //赋值式函数 alert("执行了赋值式函数"); } Fn();//执行结果:"执行了赋值式函数",同名函数后者会覆盖前者 </script>
同理当提前调用声明函数时,也存在同名函数覆盖的情况。
<script type="text/javascript"> Fn(); //执行结果:"执行了函数2",同名函数后者会覆盖前者 function Fn(){ //函数1 alert("执行了函数1"); } function Fn(){ //函数2 alert("执行了函数2"); } </script>
代码块
JavaScript中的代码块是指由<script>标签分割的代码段。JS是按照代码块来进行编译和执行的,代码块间相互独立,但变量和方法共享。如下:
<script type="text/javascript">//代码块一 var test1 = "我是代码块一test1"; alert(str);//因为没有定义str,所以浏览器会出错,下面的不能运行 alert("我是代码块一");//没有运行到这里 var test2 = "我是代码块一test2";//没有运行到这里但是预编译环节声明提前了,所以有变量但是没赋值 </script> <script type="text/javascript">//代码块二 alert("我是代码块二"); //这里有运行到 alert(test1); //弹出"我是代码块一test1" alert(test2); //弹出"undefined" </script>
上面的代码中代码块一中运行报错,但不影响代码块二的执行,这就是代码块间的独立性,而代码块二中能调用到代码一中的变量,则是块间共享性。
但是当第一个代码块报错停止后,并不影响下一个代码块运行。当然在下面的例子中,虽然代码块二中的函数声明预编译了,但是在代码块1中的函数出现Fn函数为定义错误(浏览器报错,并不是声明未赋值的undefined),说明代码块1完全执行后才执行代码块2。
<script type="text/javascript">//代码块1 Fn(); //浏览器报错:"undefined",停止代码块1运行 alert("执行了代码块1");//未运行 </script> <script type="text/javascript">//代码块2 alert("执行了代码块2");//执行弹框效果 function Fn(){ //函数1 alert("执行了函数1"); } </script>
step 1. 读入第一个代码块。
step 2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到step5。
step 3. 对var变量和function定义做“预编译处理”(永远不会报错的,因为只解析正确的声明)。
step 4. 执行代码段,有错则报错(比如变量未定义)。
step 5. 如果还有下一个代码段,则读入下一个代码段,重复step2。
step6. 结束。
<script type="text/javascript"> alert("first"); function Fn(){ alert("third"); } </script> <body onload="Fn()"> </body> <script type="text/javascript"> alert("second"); </script>
自执行函数
也就是在函数名后添加括号,函数就会自执行。在绑定事件时,像我这样的初学者有时会犯如下的错误,window.onclick = ab();这样函数ab一开始就会执行。正确的做法应该将ab后的括号去掉。而这种加括号的做法其实是把ab函数运行的结果赋值给点击事件。
下面两个例子清楚地反映了函数赋值后的情况。
1:
function ab () { var i=0; alert("ab"); return i; } var c=ab();//执行ab函数 alert(typeof c+" "+c);//number 0
2:
function ab () { var i=0; alert("ab"); return i; } var c=ab;//只赋值 alert(typeof c+" "+c);//function function ab () {var i=0;alert("ab");return i;}
注:但是这个函数必须是函数表达式(诸如上文提到的赋值式函数),不能是函数声明。详细请看:js立即执行函数:(function(){...})()与(function(){...}())
文中主要讲到匿名函数的自执行方法,即在function前面加!、+、 -甚至是逗号等到都可以起到函数定义后立即执行的效果,而()、!、+、-、=等运算符,都将函数声明转换成函数表达式,消除了javascript引擎识别函数表达式和函数声明的歧义,告诉javascript引擎这是一个函数表达式,不是函数声明,可以在后面加括号,并立即执行函数的代码(jq使用的就是这种方法)。举例如下所示。
(function(a){ console.log(a); //firebug输出123,使用()运算符 })(123); (function(a){ console.log(a); //firebug输出1234,使用()运算符 }(1234)); !function(a){ console.log(a); //firebug输出12345,使用!运算符 }(12345); +function(a){ console.log(a); //firebug输出123456,使用+运算符 }(123456); -function(a){ console.log(a); //firebug输出1234567,使用-运算符 }(1234567); var fn=function(a){ console.log(a); //firebug输出12345678,使用=运算符 }(12345678)
其作用就是:实现块作用域。
javascript中没用私有作用域的概念,如果在多人开发的项目上,你在全局或局部作用域中声明了一些变量,可能会被其他人不小心用同名的变量给覆盖掉,根据javascript函数作用域链的特性,使用这种技术可以模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以( function(){…} )()内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”。代码如下:
function test(){ (function (){ for(var i=0;i<4;i++){ } })(); alert(i); //浏览器错误:i is not defined } test();
可以对比最开始介绍作用域时候的代码。
闭包(Closure)
闭包对于初学者来说很难,需要学习很多很多才能领会,所以也是先把作用域链和匿名函数的知识作为铺垫。我这里的闭包内容属于基础篇,以后可能会贴一些更为核心的内容。我这里参照了大神们的讲解来说。参考引文:学习Javascript闭包(Closure),JavaScript 匿名函数(anonymous function)与闭包(closure),浅析作用域链–JS基础核心之一
闭包是能够读取其他函数内部变量的函数,所以在本质上,闭包将函数内部和函数外部连接起来的一座桥梁。
闭包是在函数执行结束,作用域链将函数弹出之后,函数内部的一些变量或者方法,还可以通过其他的方法引用。
两个用处:一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
为了帮助理解,我找了几个例子:
1.(阮一峰老师的讲解)
function f1(){ var n=999; nAdd=function(){n+=1} function f2(){ alert(n); } return f2; } var result=f1(); result(); // 999 nAdd(); result(); // 1000
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
2.(某大神)
function foo() { var a = 10; function bar() { a *= 2; return a; } return bar; } var baz = foo(); alert(baz()); //20 alert(baz()); //40 alert(baz()); //80 var blat = foo(); alert(blat()); //20
现在可以从外部访问 a;
a 是运行在定义它的 foo 中,而不是运行在调用 foo 的作用域中。 只要 bar 被定义在 foo 中,它就能访问 foo 中定义的变量 a,即使 foo 的执行已经结束。也就是说,按理,"var baz = foo()" 执行后,foo 已经执行结束,a 应该不存在了,但之后再调用 baz 发现,a 依然存在。这就是 JavaScript 特色之一——运行在定义,而不是运行的调用。
其中, "var baz = foo()" 是一个 bar 函数的引用;"var blat= foo()" 是另一个 bar 函数引用。
用闭包还可实现私有成员,但是我还没理解,所以就先不贴出来,想看的请参照参考引文:JavaScript 匿名函数(anonymous function)与闭包(closure)。
结束
第一次写这么长的文章,大部分是引用,但是所有内容都是亲自实践并思考后才贴出来,作为初学者可能有解释和引用不当的地方,还请大家指出。有问题的地方还请各位老师同学多来指教探讨。
再次感谢所有引文作者,知识的增长在于传播,感谢辛苦的传播者。
参考文献:
JavaScript 开发进阶:理解 JavaScript 作用域和作用域链,
js立即执行函数:(function(){...})()与(function(){...}()),
JavaScript 匿名函数(anonymous function)与闭包(closure)