浅析做用域链–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 操做符哦。

局部做用域的位置通常是在函数或者对象内部,为了叙述方便,接下来就只以函数的局部做用域来进行分析说明。闭包

在函数中使用var操做符定义一个变量,那么当这个函数执行完毕以后,这个变量也会被销毁(也有的状况下不会,好比闭包,后面会说明),而全局变量会一直存在。因此在咱们写代码时,尽可能少的使用全局变量,滥用全局变量,简直就是一个会使人恶心的习惯,由于它会带来不少没必要要的麻烦。app

  • 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结束后,并无被销毁,这也就是闭包了。

2014.10.19-PS:发现上面的这段代码,是有问题的,这样的写法,在运行时,i的值会一直是undefined,由于这个时候,i是在AlertP内部和全局做用域中查找,而这两个做用域中,并无i的定义,正确的写法,在文章的后面有说明,因此如今想不到当时为何会这么写了。。汗一个~~
PS:闭包,说白了也就是在函数执行结束,做用域链将函数弹出以后,函数内部的一些变量或者方法,还能够经过其余的方法引用。

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();

这里,objiAlertP函数内部,就是局部变量了。click事件的回调函数,虽然依旧没有变量i的值,可是其父做用域AlertP的内部,倒是有的,因此能正常的显示了,这里AlertP我放在了bindClick的内部,只是由于这样能够减小必要的全局函数,放到全局也不影响的。

这里是添加了一个函数进行绑定,若是我不想添加函数呢,固然也能够实现了,这里就要说到自执行函数了。说到自执行函数,不知道你们有什么理解,曾经有段事件,我实在是理解不到那种写法,为什么叫作自执行函数,这里也顺便带一笔了。

有没有人,在刚开始接触到JS时,会这样绑定事件:obj.onclick = callback();

而后出错了却一直找不到错误在哪里,后来才以后,当一个函数名添加了括号以后,就是函数执行了,那么也就明白了,上面的写法,其实就是把callback函数执行后的返回结果做为了objclick事件的回调函数了。

而函数名的话,也就是一个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();

测试一下也就知道了,分别为12,由于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);的不一样。固然对于第二种写法,咱们可使用callapply强行改变A内部this的指向,不过这些跟本文的内容,貌似没有什么关系,就很少说了。

其实,按照我原本的想法,这里该写一下计时器(setTimeout,setInterval)和call,apply这几个和做用域链的关系,可是写到这里,又感受他们的并无什么关系,因此关于做用域链,就到这里。

OK了,若是您有什么新的想法,或者认识,或者发现文中的错误,请指教,很是感谢!

相关文章
相关标签/搜索