一 做用域相关
做用域是一套规则,用来管理引擎如何查找变量。在es5以前,js只有全局做用域及函数做用域。es6引入了块级做用域。可是这个块级别做用域须要注意的是否是{}的做用域,而是let,const关键字的块做用域。es6
1做用域
1.1 全局做用域
在全局环境下定义的变量,是挂载在window下的。以下代码所示:面试
1.2 函数做用域chrome
在函数内定义的变量,值在函数内部才生效,在函数外引用会报RefrenceError的错误segmentfault
注意区分RefrenceError及TypeError。RefrenceError是在做用域内找不到,而TypeError则是类型错误。若是只是定义了变量a 直接调用便会报TypeError的错误。浏览器
1.3 块做用域闭包
es新增的关键字let,const是做用在块级做用域。可是在js内{}造成的块,是不具备做用域的概念的。以下所示,虽然for循环有一个{}包裹的块,可是在块外面仍是能够访问i的。异步
2 做用域链函数
所谓做用域链,是由当前环境与上层环境的一系列变量对象组成,它保证当前执行环境对符合访问权限的变量和函数的有序访问。而做用域的最大的用处就是隔离变量,不一样做用域下同名变量不会有冲突。测试
如上图所示,会造成一个inner做用域到outer做用域到全局做用域的做用域链。当咱们在执行inner函数的时候,须要outName的变量,在本身的做用域内找不到,便会顺着做用域链往上找,直到找到全局做用域。在这个例子中,往上查找到outer做用域的时候便找到了。this
简单测试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中引进了异步的机制。这块具体的执行逻辑能够参考https://segmentfault.com/a/11...。在这里,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的变化。