作用域链是个什么东东,看完下面的图就知道了
在这之前,我们先了解一些基础概念:
- 作用域:作用域是指程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,及作用域控制着变量和函数的可见性和生命周期。
- 作用域的本质,就是当前执行上下文的一个变量对象。该作用域中的变量,是以属性的形式存放在这个变量对象中
《高程》中,这样写道:在全局作用域中,这个对象叫变量对象,在函数作用域中,这个对象叫做活动对象。
下面为了方便理解,统称为变量对象
- 作用域分为全局作用域,函数作用域,块作用域,
eval作用域、with作用域,这几种,我们这篇先讨论前两种
关于执行上下文,不仅仅只有这个存放变量的对象,还有
this等东西
-
作用域形成时机:
- 当对文件顶部代码完成编译后,会生成全局执行上下文,同时就形成了全局作用域;
- 当执行一个函数时,会先对函数的代码进行编译。编译完成之后,就会生成函数的执行上下文,同时就形成了函数的作用域
- 对于块作用域,会在代码执行到该代码块的时候,生成。
图解作用域链
我们来分析这样一个代码,这里我们省略了变量和函数声明的描述
1
首先,这个代码是在写全局作用域中的,所以在这个代码编译的时候,会创建全局上下文,同时全局的变量对象也就产生了,这个对象中有this、window、document等变量,也会有在全局中声明的变量outer和a。
2
在全局代码编译过程中,声明了outer函数。这时,outer函数就会有一个[[Scope]]的属性,这个属性指向了一个作用域链(下面简称函数体中指向的作用域链),这个作用域链只有一个对像,这个对象就是全局上下文中的变量对象。这个变量对象中会有this、window、document、a等变量
上图中的a还是undefined,因为代码只是编译完成,没有得到执行,所以变量a并没有被绑定初始值
我们把变量的初始化叫做绑定初始值,和赋值有些区别
3
当代码执行到第10行的时候,调用outer函数。这个时候,编译器会编译outer中的代码。编译的时候,会生成outer的执行上下文。与此同时,会复制outer函数中的[[Scope]]来创建对应的作用域链(下面简称执行上下文的作用域链)。接着,会创建outer函数的变量对象,将其推入作用域链的前端。
该变量对象中,会预先放入arguments的变量。如果该函数有形参,也会放进其中。接着会放入var或者function声明的变量,即a和inner
注意:函数和函数的执行上下文是两个不同的对象,所以,其中的作用域链也不是同一个
上图中,可以看到this是函数编译完成之后,被加到该函数的执行上下文中的。函数体的属性是没有this的
如果当
outer中代码执行的时候,想要去访问打印a的值,那么JS引擎会在当前作用域中查找a,当前作用域就是作用域链第0的位置所指的变量对象。如果没有查找到a,就会顺着作用域链往下找,也就是往索引为1的位置所指的对象里面找。如果在全局作用域里面还没有找到a,就会报错ReferenceError: a is not defined。可惜,当前作用域中就已经有
a了,你看不到这么有趣的过程了🙈
变量对象中是否存在arguments?这个ECMAScript中是有的,但是V8似乎会优化它。
当代码中没有用到arguments的时候,V8生成的变量对象中就不会有arguments(这个只是暂时的推测,具体问题还得看代码的反编译)
4
当JS引擎编译outer函数的代码,会声明inner函数。inner函数对应的[[Scope]]属性也被赋予了值--作用域链。这个作用域链有两个对象,第0的位置是outer函数的变量对象,第1的位置是全局上下文的变量对象
这里,在第0的位置就是outer函数的闭包。对的,闭包在编译期间就产生了,和执行期的行为无关。下面有见到闭包的更详细的介绍
5
当代码执行到第6行的时候,inner函数被调用。编译器会编译inner函数中的代码。编译的时候会生成inner函数的执行上下文,与此同时,会复制inner函数的函数作用域链,来创建对应的执行上下文作用域链。接着,会创建inner函数的变量对象,并将其推入作用域链的前端。这个变量对象,会被放入arguments、a变量
在整个JS代码运行的过程中,堆中只会有一个
outer函数的执行上下文,也只会有一个全局的执行上下文。也就是说
outer作用域链上的outer变量对象,和inner函数作用域链上的,是同一个。引用不同,但是指向的对象是同一个。这个很好理解,没有必要在堆中创建两个一模一样的对象。
要记住,在
inner中,是可以操作outer的变量对象中的变量,改变值之后,outer作用域中是会同步改变的。翻来覆去地说,就是想让你记住上面的结论。
如果,在inner代码执行的时候,想要去打印b的值。那么JS引擎会首先在当前作用域中查找b,如果没有查找到,就会去下一个作用域中查找--也就是索引为1的位置所指的变量对象。如果还没有查找到,JS引擎会一直往下找,直到全局作用域为止。如果在全局作用域中还没找到,就会报错。
当然,如果在顺着作用域链查找的过程,中找到了b,就会立即停止查找的动作,并将这个变量的值返回
可以,知道了代码的我们,会知道,如果真的要打印b,JS引擎一定会报错的😉
在作用域查找变量的过程中,查找的顺序像一个链条,所以我们把这个穿起来的作用域叫做作用域链。
概念讲述完毕。
6
当代吗执行到第7行的时候,inner函数执行完毕。这个时候,inner的执行上下文会被销毁,对应的变量对象也被销毁了。这个过程和inner函数的函数作用域链没半毛钱关系
函数的执行上下文被销毁了,对应的变量对象也被销毁了。
但是不会影响作用域链上的其他两个对象--
outer变量对象、全局变量对象,它们的销毁有自己的条件。
即使
inner函数的执行上下文被销毁了,inner函数体中作用域链函数完全不受影响。
别感到伤心😄,当
inner函数被再次调用的时候,会再次创建inner的执行上下文的
7
当代码执行到11行的时候,outer函数执行完毕。outer的执行上下文会被销毁,对应的变量对象也被销毁了
全局上下文还没有被销毁,所以outer函数还是存在的
等等。这个时候,可能就有人问了,
inner函数中的函数作用域链中,是不是也没有了对outer变量对象的引用了。
其实,随着
outer的执行上下文被销毁的同时,outer作用域也被销毁了。也就是说,其中的inner函数也被销毁了。上面提出来的问题也就不存在了
8
关闭该浏览器窗口。全局上下文被销毁。
下面我们来做一个经典的面试题
为什么在函数内部可以访问函数外部的变量,而在函数外面不能访问函数内部?
-
第一种解释:函数作用域链的访问顺序,是从第0的位置,往更大索引的位置访问的,所以作用域链的查找顺序只能由内而外,不能由外而内
-
第二种解释:访问内部函数的变量有两个位置。一个是内部函数执行之前,一个是内部函数执行之后。那么在内部函数执行之前,内部函数的执行上下文都没有生成,谈何访问内部的变量?在内部函数执行完成之后,内部函数的执行上下文都被销毁了,又谈何访问内部的变量?
怎么样,有没有一种开脑洞的赶脚呢?
下一篇文章的内容,是关于闭包的(JS-图解闭包),也很精彩啊。快去看看。
总结
- 简述了什么是作用域
- 图解了作用域链
- 下一章节,我们来通过这个作用域链,来讲解闭包是如何产生的。当然也是超简单😎
- 如果我哪里讲的不明白,一定要告诉我,谢谢!