结合作用域,执行上下文图解闭包

一 作用域相关
      作用域是一套规则,用来管理引擎如何查找变量。在es5之前,js只有全局作用域及函数作用域。es6引入了块级作用域。但是这个块级别作用域需要注意的是不是{}的作用域,而是let,const关键字的块作用域。

1作用域
1.1 全局作用域
      在全局环境下定义的变量,是挂载在window下的。如下代码所示:
图片描述

1.2 函数作用域

      在函数内定义的变量,值在函数内部才生效,在函数外引用会报RefrenceError的错误
图片描述

      注意区分RefrenceError及TypeError。RefrenceError是在作用域内找不到,而TypeError则是类型错误。如果只是定义了变量a 直接调用便会报TypeError的错误。
图片描述
图片描述

1.3 块作用域

      es新增的关键字let,const是作用在块级作用域。但是在js内{}形成的块,是不具有作用域的概念的。如下所示,虽然for循环有一个{}包裹的块,但是在块外面还是可以访问i的。
图片描述

2 作用域链

      所谓作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证当前执行环境对符合访问权限的变量和函数的有序访问。而作用域的最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
图片描述

      如上图所示,会形成一个inner作用域到outer作用域到全局作用域的作用域链。当我们在执行inner函数的时候,需要outName的变量,在自己的作用域内找不到,便会顺着作用域链往上找,直到找到全局作用域。在这个例子中,往上查找到outer作用域的时候便找到了。

      简单测试1:如下图所示的代码,大家觉得会输出什么呢?
图片描述
      虽然fn的调用是在show内调用的,但是因为fn所在的作用域是全局作用域,它的x的值会顺着作用域链去全局作用域中啊,即x会输出10。这里需要注意的一点是,变量的确定是在函数定义时候确定的,而不是函数运行时。

二 执行上下文相关

      函数每次被调用时,都会产生一个新的执行上下文环境。全局上下文是存在栈中的。而处于栈顶的全局上下文一旦执行完就会自动出栈。如下图所示的代码。
图片描述

      首先是全局上下文入栈,然后开始执行可执行代码。遇到outer(),激活outer()的上下文;

      第二步,outer的上下文入栈。开始执行outer内的可执行代码,直到遇到inner()。激活inner()的上下文;

      第三步,inner的上下文入栈。开始执行inner内的可执行代码。执行完毕之后inner出栈。

      第四步,inner的上下文出栈。outer内继续执行可执行代码。如果一直没有其他的执行上下文,执行完毕即可出栈;

      第五步,outer的上下文出栈。

      ps:全局上下文只有浏览器关闭的时候才会出栈。

图片描述

      那我们已经直到了全局上下文的宏观入栈出栈的概念。具体的全局上下文包括哪些内容,具体做了什么操作呢?

      其实,执行上下文分为准备阶段和执行阶段。

      1.在执行上下文的准备阶段,会有以下步骤:

            1.1 创建变量对象:初始化arguments,函数声明提升,变量声明提升等

            1.3 建立作用域链

     2.而在执行上下文的执行阶段,会有以下步骤:

            2.1 变量赋值

            2.2 函数引用

            2.3 确定this指向

            2.4 执行代码

      而在变量对象的创建过程,会经历以下的步骤。

            1.创建arguments对象。也就是当前上下文中的参数;

            2.检查当前上下文的函数声明,即用function关键字声明的函数;

            3.检查当前上下文的变量声明,即变量,属性值为undefined。

      而这个创建过程最重要的概念就是提升:
图片描述

      而如下图所示的代码执行,变量对象的变化过程是怎样的呢?
图片描述
      那函数内的三个console分别会输出什么呢?
      因为在变量对象的创建过程中,是arguments=>函数声明=>变量声明的过程。在第一个console之前function foo()已经被提升,因此第一次输出的该函数,而第二个console之前bar被提升,并赋值为undefined,因此第二次输出的是undefined。而第三个console之前foo被重新赋值,因此第三个console是'hello'。

      总结起来,变量对象和活动对象其实是同一个对象,他们只是在执行上下文的不同阶段的状态而已。
图片描述
      下面的截图即是两个阶段的变化。其实变量对象和活动对象是同一个对象,他们只是执行上下文在不同阶段的不同表现形式。在执行阶段变量对象V0会变成活动对象A0。内部的一些引用也会发生变化。
图片描述
      而如下图所示的代码执行,分别会输出什么呢?
图片描述

      首先,第一段代码。函数声明首先会被提升第一个console输出hello world。但是后面的hello会被覆盖,第二个console输出hello

      第二段代码。函数声明首先会被提升,但是紧接着会被变量赋值覆盖。因此,两个console输出hello。
总结起来,全局上下文的整个过程即下图所示

图片描述

      那结合作用域即全局上下文呢,我们一开始的代码代码具体的图解就是下面这张图了。

图片描述

三 闭包相关
1 闭包分析

      此时,当我们修改inner函数,返回上级作用域的outerName属性时,闭包就产生了。

图片描述

      这里为什么会产生闭包呢?具体可以参考下方的图示。
      前面的全局入栈和outer函数入栈还是跟原来一样,但是当我们的outer函数入栈执行完毕准备出栈,准备被回收的时候,由于outName还被inner的作用域引用,不能被回收,产生了闭包。
图片描述
      即所谓的闭包就是通过函数调用,外部持有函数的句柄,让函数的空间不能消失。产生的这块独体的空间永远存在,这块内存对外也是封闭的。所以就叫闭包。

2 常见问题分析

      相信大家在面试的时候会经常问到这样的面试题。下面这段代码输入的是什么呢?
图片描述

      这里输出的是5个6。需要解释这个问题呢,要涉及到js的的执行环境及作用域链了。

      js的执行环境:JS是单线程环境,即代码的执行是从上到下,依次执行。这样的执行称为同步执行。因为种种不要浪费和节约的原因。JS中引进了异步的机制。这块具体的执行逻辑可以参考http://km.oa.com/articles/sho...。在这里,for循环是同步代码,会先从上到下执行。而setTimeout中的是异步代码会将其插入到任务队列当中等待。因此在setTimeout执行的时候,for循环已经执行完成,i已经变成6。作用域链。当setTimeout执行的时候,会向上去查找i的值。往上查找,即for所在的作用域,已经是6了。因此6次setTimeout都会输出6。

      那可能面试官会继续问,我们怎样才能依次输出1-5呢?这里就可以用到闭包来解决了。
图片描述

      我们将i作为参数传递,并且形成了一个新的立即执行函数作用域。当setTimeout执行的时候,去查找i。即在立即执行函数作用域查找,此时的i我们可以根据上面一部分的分析,形成了闭包之后,它的内存是不会消失的。因此这每次循环的时候都是当前i即1-5。

3 闭包的查看

      其实,我们在chrome的控制台是可以去查看闭包的。在浏览器断点调试,可以去观察下面两幅图的红色圈区别。第二副图可以看到closure,i值是1。依次执行,可以看到i从1到5的变化。

图片描述

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章