作用域链–JS基础核心之一
JS
中的作用域,大家都知道的,分为全局作用域和局部作用域,没有块级作用域,听起来其实很简单的,可是作用域是否能够有深入的了解,对于JS
代码逻辑的编写成功率,BUG
的解决能力,以及是否能写出更优秀的代码,都有很重要的影响的,如果想要写出更优雅更高效的逻辑代码,那么就要深入的了解一下作用域的问题了,确切的说,是要更深入的了解一下,怎么更有效更巧妙的利用作用域。
全局和局部作用域
这个我觉得吧,只要学习过编程语言的,就会对这些有简单的了解的。比如在JS
语言中,属于window
对象的属性和方法,是可以被我们自定义的函数或者方法的局部作用域访问的,而我们自定义的函数和对象内部的属性和方法,却只能在内部使用。这里,window
对象就是在全局作用域中,而我们自定义的函数或者对象内部,就是局部作用域。
- var num = 1;
- function changeNum(){
- var str = "zhang";
- num = 2;
- }
- console.log(num); //1
- console.log(typeof str);//undefined
- changeNum();
- console.log(num); //2
- console.log(typeof str);//undefined
上述代码中,之所以要使用typeof str
,是因为对于没有定义的变量,浏览器会抛出错误,并且阻塞浏览器继续执行后续代码的。
局部作用域的位置一般是在函数或者对象内部,为了叙述方便,接下来就只以函数的局部作用域来进行分析说明。
在函数中使用var
操作符定义一个变量,那么当这个函数执行完毕之后,这个变量也会被销毁(也有的情况下不会,比如闭包,后面会说明),而全局变量会一直存在。所以在我们写代码时,尽量少的使用全局变量,滥用全局变量,简直就是一个会令人恶心的习惯,因为它会带来很多不必要的麻烦。
- 1:变量过多,命名麻烦
- 2:局部变量,忘记使用var定义,修改了全局变量,这样的错误对于代码的维护简直是噩梦
- 3:全局变量会在页面卸载前一直存在,损耗不必要的内存。
暂时就想到这些,反正就是尽量少用就对了。。。。
作用域链
引自Javascript
高级程序设计(第三版)(P73
):当代码在一个环境中执行时,会创建变量对象的的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是一个函数,则将其活动对象作为变量对象。
每一个函数都有自己的执行环境,当执行流进一个函数时,函数环境就会被推入一个环境栈中,而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,这个栈也就是作用域链。
上面写了那么多,在我看起来可以用下面的简单代码来表达:
- var a = 1;
- //全局作用域,只能访问全局变量,也就是a变量
- function A(){
- var b = 2;
- //A函数的局部作用域,可以访问到a,b变量,但是访问不到c变量
- function B(){
- //B函数局部作用域,可以访问到a,b,c变量
- var c = 3;
- }
- }
很明显的,貌似作用域方面,也没有什么好说的。可是,有时候,我们却不得不去访问一些局部作用域内部的东西,比如两个模块函数,使用了相同的数据,这里我们也只能把这些相同的数据放入全局变量,使得两个函数模块,都可以调用这些数据。
但是想想,如果这样的需求很多,那么不久需要很多很多的全局变量,而滥用全局变量的不好之处,前面也说了,所以这并不是一种好的写法。
减少全局变量
减少全局变量的方法,其实也很多,比如把一些相同类型的全局变量存入一个对象,那么就可以把这些类型的N多个全局变量,变成一个全局的对象,之后按照对象访问即可。
当然,我觉得吧,最简单,又好用的,还是在一个函数内部,继续定义函数,就像之前在函数A
内部,定义了函数B
,这样我们只需要一个函数A
的执行,就可以完成一整个逻辑。内部的调用,都只能算是局部变量的调用,在全局只添加了一个函数A
。
比如:
- function A(){
- var arr = [];
- function a(){};
- function b(){};
- return;
- }
这样,我们本来需要三个全局变量的问题,就变成了只需要一个。当然,如何减少全局变量的方法是有很多种的,这里不做讨论。
这里,我们就讨论一种我们最常见的方法,也算是很常用的一种代码书写方法吧,它叫:闭包。
减少全局变量方法–闭包
说到闭包,我们首先来看一个最最简单的例子,也是最最基础的例子:为多个相同的元素,绑定事件,在点击每一个元素时,提示被点击元素的排列位置。
- <div id = "test">
- <p>栏目1</p>
- <p>栏目2</p>
- <p>栏目3</p>
- <p>栏目4</p>
- </div>
这样的结构
- 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();
- //运行函数,绑定点击事件
这样的JS
处理,看起来没有问题,可是在测试的时候,不管我们点击哪一个p
标签,我们获取到的结果都是相同的,tell me why?说白了,这就是作用域到导致的一个问题。
下面来分析一下原因。首先呢,我们先把上述的JS
代码给分解一下,让我们看起来更容易理解。
- 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
。所以也就出现了我们最初看到的效果。
了解了这里的原因,那么解决方法也就很简单了,控制这个作用域的问题呗,说白了,也就一个方法,那就是在回调函数中,用一个局部变量,来记录这个i
的值,这样当再局部作用域中使用到i
变量时,就会使用优先使用局部变量中的i
变量的值。不会再去查找全局变量了。
所以呢,理解了这两段文字,那么如果我把代码写成下面的样式:
- 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();
- //运行函数,绑定点击事件
分析一下,如果这段代码这样写,那么结果会是如何呢?
说到了这里,大概也能理解一下闭包的概念了,按照之前我们说的作用域链的说法,当一个函数运行时,该函数就会被推入作用域链的前端,当函数执行结束,这个函数就会被推出作用域链,并且销毁函数内部的局部变化和方法。
但是这里呢,当bindClick
运行结束后,依然可以通过click
事件访问到bindClick
函数内部的i变量,说明bindClick
函数内部的i
变量,在bindClick
结束后,并没有被销毁,这也就是闭包了。
OK,回到正题,这里既然知道了需要一个局部变量的i
值,可以解决这个问题,那么方法也就很简单了,按我们之前说的,变量按照可访问性的话,只分为全局变量和局部变量,那么这里的就很简单了,使用一个函数,构造一个局部变量即可。
方法1:使得绑定click
事件的目标对象和变量i都变成局部变量。这里可以直接把这两者作为形参,传递给另外的一个函数即可。
- 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
时,会这样绑定事件:obj.onclick = callback();
然后出错了却一直找不到错误在哪里,后来才之后,当一个函数名添加了括号之后,就是函数执行了,那么也就明白了,上面的写法,其实就是把callback
函数执行后的返回结果作为了obj
的click
事件的回调函数了。
而函数名的话,也就是一个function
函数的引用吧,根据函数名查找到对应的function
处理模块,所以这里很容易的也就想到了,自执行函数也就是直接在一个匿名函数的后面添加一对小括号,那么这个匿名函数就会自己执行了。所以也就是自执行函数了。
比如我们在页面加载之后,想要立即提示用户,页面加载完毕,我们习惯于这么写:
- function loadSuccess(){
- alert("page onload success!");
- }
- loadSuccess();
这是我们常用的方法,这里首先定义个函数,并把函数名命名为loadSuccess
,之后调用这个函数。很常用很简单。
这里我们通常也可以使用自执行函数来完成这个提示,你就可以这样写:
- (function(){
- alert("page onload success!");
- })();
完成相同的功能,这里必须把这个匿名函数放在小括号内部,不然浏览器会报错的。
原因呢,也是JS
中的常识之一,那就是function A(){}
这样的定义函数的方法,会在浏览器进行预编译的时候进行解析,而var A = function(){}
这样的定义函数的方法,则是当JS
解析到该行代码时,才会被解析。
这里呢,如果在上面的自执行函数中,不添加第一个小括号,浏览器就会在预编译时,对该部分进行解析,但是这个时候,因为没有对这部分function
进行命名,浏览器在预编译时就会报错,而导致无法进行下去了。
使用下面这段函数,就可以证明,是在预编译的时候,报错的而导致无法执行的
- alert("123");
- function(){
- alert("page onload success!");
- }();
当然啦,加括号本就不是必须的,比如我们使用表达式定义函数时,var A = function(){}
这种写法,就不是在预编译的时候进行的,所以,如果我们的自执行函数会把返回值定义到另外一个变量,是可以省略掉小括号的。
比如:
- alert("123");
- var a = function(){
- alert("page onload success!");
- }();
这样写也会连续有两个alert
执行,完成我们之前说的功能,也不会报错,只是这时,自执行函数是没有返回值的,所以最后的a
变量,是undefined
。不过呢,为了统一起见,也为了看着方便,所以还是对各种写法的自执行函数的写法,都添加上小括号吧。
至于为什么,添加了小括号()()
,这样写,就可以,那就是因为,这样的写法就变成一个表达式了。。。。
可以这么证明一下:
- (function A(){
- alert("page onload success!");
- });
- A();
只是这样的写法,和表达式定义函数就类似了,而且还会有一个问题就是,A
函数,只有在这个括号内部使用。在外部使用,需要先把这个表达式进行赋值才行,如果赋值,那不就是成了使用赋值表达式定义函数了。
说的远了点,回来继续:到这里也大概了解了自执行函数的执行方法了吧。那使用自执行函数的方法,进行事件的绑定,大概也能猜到它的原理了吧。obj.onclick = callback();
。如果我把callback
函数的返回值,定义成一个函数,那当click
事件触发时,不就是触发了这个返回的函数了。
所以呢,我们可以这样写:
- function bindClick(){
- var allP = document.getElementById("test").getElementsByTagName("p"),
- i=0,
- len = allP.length;
- for( ;i<len;i++){
- allP[i].onclick = AlertP(i);
- }
- }
- function AlertP(i){
- return function(){
- alert("you click the "+i+" P tag!");
- }
- }
- bindClick();
没有什么问题吧?应该很容易理解到吧。
可是这样的写法呢,添加了一个函数变量,如果不添加呢。。。OK的,把后面的函数直接替换过去就行了。。。。
- function bindClick(){
- var allP = document.getElementById("test").getElementsByTagName("p"),
- i=0,
- len = allP.length;
- for( ;i<len;i++){
- allP[i].onclick = function (i){
- return function(){
- alert("you click the "+i+" P tag!");
- }
- }(i);
- }
- }
- bindClick();
这样看起来,对比之前的写法,应该就能很明显的了解到,为什么这么写,能得到我们想要的结果了吧。
OK,这也是闭包的最简单的应用了,其他的闭包写法也有,只是就原理方面来说,和上面这种是相同的原理,所以这里就不一一列举了,用到闭包的地方其实很多(比如惰性载入函数,单例模式中的对象定义等),如果您能理解到这最简单闭包的原理,那么其他用到闭包的地方,见到了,也就能理解了。或者说,想要使用的时候,也就能想到应该怎么用了吧。
之前的文章中,也有一篇文章中的代码,主要就是使用的闭包的思想,可以参考:jQuery源码学习(二)–proxy
备注
计时器在一些动态页面,做一些动画效果时,是不可或缺的一个元素,它和alert
方法相同,都是属于window
对象的方法。使用计时器时,是有少许差别的,这里就以setTimeout
为例简单说明:
看例子:代码中中的两个setTimeout
执行后的结果分别是什么?
- var a = 1;
- function B(){
- var a = 2;
- setTimeout("C()",1000);
- setTimeout(C,2000);
- function C(){
- alert("a="+a);
- }
- }
- function C(){
- alert("a="+a);
- }
- B();
测试一下也就知道了,分别为1
和2
,因为setTimeout
是把后面执行的方法,第一种写法,只会查找全局变量中,是否有A
函数,而第二种写法,会优先查找当前作用域中是否有A
函数,如果局部没有的话,则顺序查找到全局作用域中。
有一种情况,是说,计时器内部调用的函数的this
指向,是指向window
的,这里可以说有错,也可以说没错,看一个例子:假设给id=test
的一个元素绑定一个click
事件。查看其中的this
的值。
- document.getElementById("test").onclick = function(){
- alert(this); //指向触发该事件的元素对象
- setTimeout("A()",1000); //这里调用指向window
- }
- function A(){
- alert(this);
- }
这里就不考虑在IE8-
的浏览器了。
按照最初写的两个计时器的例子,在写出如下的代码:
- document.getElementById("test").onclick = function() {
- alert(this); //指向触发该事件的元素对象
- setTimeout(A,1000); ////这里依然指向window
- function A(){
- alert(this);
- }
- };
- function A(){
- alert(this);
- }
为什么?不是按理说,这里应该是调用的内部的A
方法吗?为什么this
却是指向的window
?
有一个不确定的想法是:当调用了计时器时,会把当前作用域中的方法,内部的this
指向window
对象了。而且仅仅是修改了方法内部的this
指向,如果有私有变量的取值,依然按照原函数所在的位置,根据作用域,进行取值。
可以这么证明一下:
- var a = 1;
- document.getElementById("test").onclick = function() {
- alert(this);
- var a = 123;
- setTimeout(A,1000);
- function A(){
- alert("a="+a);
- alert(this);
- }
- }
- function A(){
- alert("a="+a);
- alert(this);
- }
this
的指向是和上面一个实例相同的,而alert
中的a
变量的取值,却是优先获取局部作用域中的值。
当然啦,这里如果把计时器中的调用方法,更换一下,那结果就不相同了哦。
- var a = 1;
- document.getElementById("test").onclick = function() {
- alert(this);
- var a = 123;
- setTimeout("A()",1000);
- function A(){
- alert("a="+a);
- alert(this);
- }
- }
- function A(){
- alert("a="+a);
- alert(this);
- }
这里,有兴趣的可以试试吧,说到这里,也发现,虽然使用计时器会强制把调用函数的内部的this
指向改变成指向window
的,但是对于作用域链的影响却只有写法不同带来的影响。即:setTimeout("A()",1000);
和setTimeout(A,1000);
的不同。当然对于第二种写法,我们可以使用call
和apply
强行改变A
内部this
的指向,不过这些跟本文的内容,貌似没有什么关系,就不多说了。
其实,按照我本来的想法,这里该写一下计时器(setTimeout,setInterval
)和call
,apply
这几个和作用域链的关系,但是写到这里,又感觉他们的并没有什么关系,所以关于作用域链,就到这里。
OK了,如果您有什么新的想法,或者认识,或者发现文中的错误,请指教,非常感谢!