• 作用域链–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操作符定义一个变量,那么当这个函数执行完毕之后,这个变量也会被销毁(也有的情况下不会,比如闭包,后面会说明),而全局变量会一直存在。所以在我们写代码时,尽量少的使用全局变量,滥用全局变量,简直就是一个会令人恶心的习惯,因为它会带来很多不必要的麻烦。

    • 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了,如果您有什么新的想法,或者认识,或者发现文中的错误,请指教,非常感谢!

    本文地址:http://www.zhangyunling.com/?p=134

  • 相关阅读:
    jsp的C标签一般使用方法以及js接收servlet中的对象及对象数字
    关于Springmvc中include与Sitemesh装饰器的基本使用
    关于垃圾回收的一些基本原理及技术解析
    一张图解析 编译器编译流程
    Java解析OFFICE(word,excel,powerpoint)以及PDF的实现方案及开发中的点滴分享
    冲刺五
    冲刺四
    冲刺三
    冲刺二
    冲刺一
  • 原文地址:https://www.cnblogs.com/wxcbg/p/5845729.html
Copyright © 2020-2023  润新知