关于Javascript中的作用域和作用域链的思考和总结

posted @ Nov 12, 2015 • 前端笔记

摘要:「今天上课的时候,看到`作用域链`这个词儿,之前学Javascript的时候,还是好好理解了的,不过很快就模糊了。说明还是没有很深入理解这个Javascript中的难点!所以就花点时间整理一下这个难点知识。」

问题背景

今天上课的时候,看到作用域链这个词儿,之前学Javascript的时候,还是好好理解了的,不过很快就模糊了。说明还是没有很深入理解这个Javascript中的难点!所以就花点时间整理一下这个难点知识。

执行环境

函数大家都知道,我想说的是,js中,在函数内部有两个特殊的对象:arguments 和 this 。 arguments 是一个类数组对象,包含着传入函数中的所有参数。 this 引用的是函数据以执行的环境对象。

在《js高级程序设计》中,是这样定义的 P73页:

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

这里要特别提到一个执行环境——全局执行环境。全局执行环境是最外围的执行环境,在web浏览器中,全局执行环境被认为是window对象。全局执行环境会一直存在于环境栈的最底端,直到关闭网页或者浏览器。

当然与之对应的就是局部执行环境,局部执行环境就是函数执行环境,这也就是我们常说的局部作用域。

执行环境也叫执行上下文。

变量对象

《js高级程序设计》定义如下:

每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。

由上可以看出: 1. 执行环境和变量对象是一一对应的。 2. 执行环境其实是一个“虚”的概念,而变量对象是实际存在的对象,能够被解析器访问到。 3. 不严谨的说,为了访问执行环境,就创造了变量对象这个东东,通过变量对象就可以访问执行环境中的所有变量和函数了,它俩其实是一个东西,只不过一个是虚的,一个是真实存在的。 4. 当一个执行环境中的所有代码执行完毕后,该执行环境被销毁,保存在其中的所有变量和函数定义也随之销毁。 5. 其实是这个执行环境对应的变量对象被销毁。发现很多地方都把执行环境和变量对象混谈,大家似乎很少提到变量对象,都用 “执行环境” 这四个字替代了,把执行环境说成了一个对象,没办法,谁让说的是一个东西呢。

变量对象的创建过程

  • 当JS执行流进入函数时,JavaScript引擎在内部创建一个对象,叫做Variable Object。
  • 对应函数的每一个参数,在Variable Object上添加一个属性,属性的名字、值与参数的名字、值相同。 函数中每声明一个变量,也会在Variable Object上添加一个属性,名字就是变量名,因此为变量赋值就是给Variable Object对应的属性赋值。
  • 在函数中访问参数或者局部变量时,就是在variable Object上搜索相应的属性,返回其值。
  • 一般情况下Variable Object是一个内部对象,JS代码中无法直接访问。

作用域链

作用域链的用途:保证对执行环境有权访问的所有变量和函数的有序访问。 当代码在一个执行环境中执行时,就会创建变量对象的一个作用域链。

我的理解是,作用域链是由一个一个变量对象链接起来的一个链,整个作用域链构成了当前执行环境中变量和函数可访问的范围,即作用域。由于变量对象是按一定顺序链接在一起的,所以就达到了对所有可访问变量、函数有序访问的效果。那么它们是按怎样的顺序链接成作用域链的呢?这就要说到最后一个概念——活动对象。

活动对象

当函数运行时就会为其创建一个活动对象,其中包含形参和函数特殊的arguments对象。活动对象之后会做为函数执行环境的变量对象来使用。

回到之前的问题,作用域链中的变量对象是如何排序的呢?

作用域链的前端,始终都是当前执行的代码所在环境的变量对象。但如果这个环境是函数,则将其活动对象作为变量对象,放在其作用域链的前端。作用域链中的下一个变量对象来自包含环境,而再下一个变量对象来自下一个包含环境……这样一直延续到全局执行环境。全局执行环境的变量对象始终是作用域链中的最后一个变量对象。

为什么执行环境是函数会有这样特殊的规定呢?

《JS权威指南》中有一句很精辟的描述:

JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。

按照之前所说,在函数定义的作用域里,当前执行环境的作用域链上是没有该函数的活动对象的,为了访问函数内部的变量、函数,所以要将其活动对象插在当前作用域链的前端。

作用域链的构建过程

  • 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在执行后,栈将其环境弹出,把控制权返回给之前的执行环境。
  • 综上,每个函数对应一个执行环境,每个执行环境对应一个变量对象,而多个变量对象构成了作用域链,如果当前执行环境是函数,那么其活动对象在作用域链的前端。

延伸扩展

  • S的语法风格和 C/C++ 类似, 但作用域的实现却和 C/C++ 不同,并非用“堆栈”方式,而是使用列表,具体过程如下(ECMA262中所述):

  • 任何执行上下文时刻的作用域, 都是由作用域链(scope chain)来实现.

  • 在执行func的定义语句的时候, 会创建一个这个函数对象的[[scope]]属性(内部属性,只有JS引擎可以访问),并将这个[[scope]]属性链接到定义它的作用域链上。

  • 在调用func的时候, 会创建一个活动对象,然后将调用参数赋值给形参数,对于缺少的调用参数,赋值为undefined。然后将这个活动对象做为scope chain的最前端, 并将func的[[scope]]属性所指向的,定义func时候的顶级活动对象,加入到scope chain.


只有概念的东西,关于图和代码,明天再补上吧!

2015-09-16

作用域链的深入

上次说到作用域链,基本都是理论层面上的,之前听过妙味课堂的一些关于作用域方面的视频,再补充一下。

的字面上来看,域就是范围,空间。Javascript解析器解析代码时首先会进行预解析。预解析首先会在执行代码之前查找关键字或者叫做标识符(变量,函数,参数)。将其存入执行栈(仓库)中。然后开始逐行解读和执行代码。

那么标识符的解析就是沿着上文说的作用域链一级一级的向上回溯,从内部执行环境向外部执行环境搜索,直到window对象。如果外部也找不到这个标识符,通常就会报错,就是有我们常见的undefined。这种访问时线性的,有序的。这就是作用域链所起到的作用,每个当前执行环境都可以向上(向外)搜索作用域链,以查询变量和函数名,表达式,参数。但是不能向下访问作用域链。

当当前的执行环境执行完毕后,环境栈会将其弹出。函数内部的变量,表达式,参数连同函数本身,会被立即销毁。就是这个执行环境被销毁。此时执行环境又返还给之前的执行环境。