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