什么是作用域?作用域产生于程序源代码中定义变量的区域,在程序编码阶段确定。JavaScript分为全局作用域(Globalcontext:window/global)和局部作用域(LocalScope,又称函数作用域Functioncontext)。简单来说,作用域就是当前函数的生成环境或上下文,包括当前函数中定义的变量和对外部作用域的引用。Scope:作用域(Scope)-window/globalScope全局作用域函数Scope函数作用域BlockScope块作用域(ES6)evalScopeeval作用域定义了一组规则,这组规则定义了引擎Scope或嵌套作用域根如何查找变量通过标识符。反之,由N个作用域组成的作用域链决定了函数作用域中标识符查找后的返回值。因此,作用域决定了当前上下文中定义的变量的可见性,即子作用域可以访问当前作用域中的属性和函数。而作用域链(ScopeChain)也决定了在当前上下文中查找标识符后返回的值。作用域分为词法作用域和动态作用域。LexicalScope就像字面意思一样,就是词法阶段定义的范围。换句话说,范围是在词法分析器(lexer)处理源代码时根据变量和块在源代码中的位置设置的。JavaScript使用词法范围。作用域规则作用域限制函数内变量和函数的可访问性。函数内部声明的属性和函数属于函数的私有属性,不会暴露给函数外部的代码。同时,在函数内部声明的嵌套函数继承了对当前函数中的属性和函数的访问权限。具体规则如下:如果函数内部定义了变量a,则函数内部的其他变量可以访问变量a,但函数外部的代码不能访问变量a。因此,同一作用域内的变量可以相互访问,即a、b、c在同一作用域内可以相互访问。就好比鸡妈妈生小鸡,小鸡之间可以打架,其他的鸡就不能跟它们打架。为什么?因为鸡妈妈不允许~o(???)o。leta=1functionfoo(){letb=1+aletc=2console.log(b)//2}console.log(c)//错误globalfunctioncannotaccesscfoo()ifvariableaisglobalDefined在作用域(window/global)下,全局作用域下局部作用域中的执行代码或表达式可以访问变量a的值。局部变量中的同名变量(a)会截断对全局变量a的访问。(这里的变量a相当于饲养员,饲养员会在合适的时间给小鸡喂食,但是农夫为了节约成本,规定饲养员就近喂鸡。当饲养员1远离chickenbaby最近其他饲养员都过不了鸭绿江喂鸡了。)leta=1letb=2functionfoo(){letb=3functiontoo(){console.log(a)//1安慰。log(b)//3}too()}foo()再次强调javascript作用域严格限制了变量的可访问作用域:即根据代码和块在源代码中的位置,嵌套作用域有嵌套域(外部范围)访问权限的影响。(这个规则说明整个农场是有规则的,不能反喂。)作用域链(ScopeChain)作用域链由当前环境和上层环境中的一系列作用域组成,保证了当前的执行环境命令访问符合访问权限的变量和函数。上面的解释有点晦涩,对于我这种脑子不好使的人来说,需要在脑子里多“读”几遍才能明白。那么作用域链是做什么用的呢?简单的说,作用域链就是管理函数声明形成的作用域嵌套(依赖)关系,在函数运行阶段解析函数访问标识符的值。简单解释一下作用域链的作用:作用域链是用来查找变量的,作用域链就是一系列串联起来的作用域。作用域链访问在函数执行过程中,每遇到一个变量,都会经过一个标识符解析过程,以确定从哪里获取和存储数据。这个过程从作用域链的头部开始,即当前正在执行的函数的作用域(下图中从左到右),寻找一个同名的标识符,如果是则返回该标识符对应的值found,andcontinueifnotfound搜索作用域链中的下一个作用域,如果搜索了所有作用域都没有找到,则认为该标识符未定义。在函数执行过程中,每一个值得解析的标识符都要经过这样一个查找过程。为了具体分析问题,我们可以假设作用域链是一个数组(ScopeArray),数组成员由一系列变量对象组成。我们可以在上图模拟的数组的单向通道中,从左到右查询变量对象中的标识符,这样就可以访问上层作用域的变量了。直到最顶层(全局范围),一旦找到,就停止寻找。所以内部变量可以屏蔽同名的外部变量。想象一下,如果不从里到外查找变量,整个语言设计就会变得N复杂(我们需要设计一套复杂的小鸡寻找食物的规则)还是上面的栗子:leta=1letb=2functionfoo(){letb=3functiontoo(){console.log(a)//1console.log(b)//3}too()}foo()作用域嵌套结构是这样的:举个栗子,当javascript引擎执行函数太,全局,函数foo和函数太上下文将分别创建。上下文中包含了它们各自的变量对象和作用域链(注:作用域链中包含了上层作用域的可以访问的变量对象,在上下文创建阶段根据作用域规则进行收集,形成可访问链),我们set分别定义它们的变量对象为VO(global),VO(foo),VO(too)。too的作用域链同时包含这三个变量对象,所以too的执行上下文可以表示为:too={VO:{...},//变量对象scopeChain:[VO(too),VO(foo),VO(global)],//作用域链}我们直接用scopeChain表示作用域链数组,数组的第一项scopeChain[0]是作用域链的前端(当前函数的变量对象),数组的最后一项是作用域链的末尾(全局变量对象窗口)。请注意,所有范围链的末尾是全局变量对象。又如:leta=1functionfoo(){console.log(a)}functiontoo(){leta=2foo()}too()//如果你对作用域的特性不了解很容易以为输出是2。但实际上最终输出是1。执行foo()时,它首先在当前范围内查找变量a。然后根据函数定义时的作用域关系,会在当前作用域的上层作用域中查找变量标识a,所以最后找到的是全局作用域中的a,而不是foo函数中的a。后面会介绍变量对象和执行上下文。闭包在JavaScript中,函数和声明它们的词法范围形成闭包。或者更通俗的理解,闭包就是一个可以读取其他函数内部变量的函数。这里,闭包理解为函数内部定义的函数。让我们看一个闭包的例子leta=1functionfoo(){leta=2functiontoo(){console.log(a)}returntoo}foo()()//2这是一个闭包的例子,一个函数在执行后返回另一个可执行函数,返回的函数在定义时保留对外部函数作用域的访问。当调用foo()()时,函数foo和too会依次执行。虽然too是在全局作用域内执行的,但too是在foo作用域内定义的,根据作用域链规则取最近的嵌套作用域的属性a=2。再拿农场的故事来说。农民们发现,还有一个更划算的方法,就是让每只母鸡充当家庭成员的“饲养员”,从而改变以往的“养殖结构”。从作用域链的结构可以发现,javascript引擎在查找变量标识符时,是按照作用域链顺序向上查找的。当标识符的作用域位于作用域链中较深的位置时,读写速度相对较慢。因此,在编写代码时,应尽可能少使用全局代码,并尽可能在本地范围内缓存全局变量。如果没有内存增强,很容易记错作用域和执行上下文之间的区别。代码的执行过程分为编译阶段和解释执行阶段。应该永远记住,javascript作用域是在源代码的编码阶段确定的,作用域链在编译阶段被收集到执行上下文的变量对象中。因此,作用域和作用域链是在当前运行环境中的代码执行之前确定的。执行上下文的概念我们暂时不过多展开,大家可以关注后续文章。闭包的一些优缺点闭包的用途:用于保存私有属性:将不需要暴露给外界的属性和函数保存在闭包函数的父函数中,避免外界对值的操作干扰并且避免局部属性污染全局变量空间,由此产生的命名空间混乱,模块化封装,对面的功能模块通过闭包进行封装,只对外暴露较少的API。闭包的缺点:内存消耗:由于闭包,函数中的所有变量都会保存在内存中,内存消耗非常大,所以不能滥用闭包,否则会导致网页出现性能问题。造成内存泄漏:由于IE的js对象和DOM对象使用不同的垃圾回收方式,闭包在IE中会造成内存泄漏,即驻留在内存中的元素不能被销毁。解决方案是在退出函数之前删除所有未使用的局部变量)。编译阶段和解释执行阶段将在变量对象部分详细介绍。其他一些关于闭包的知识点也会在后面的章节中提到,请注意。最后再看一道面试题:for(vari=0;i<5;i++){setTimeout(function(){console.log(i);},1000);}//55555要求修改上面的代码,让它输出'01234'这也涉及到作用域链的概念,当然也和javascript的执行机制有关。修改的方式有很多种,这里介绍一种:for(vari=0;i<5;i++){setTimeout(function(){console.log(i);}(i),1000);}//01234详细原理分析会在javascript执行机制部分详细介绍。
