由一道题图解JavaScript的作用域

作用域

为了理解作用域,跪看了好几篇大神的博文,终于略知一二。

1.题目

其中,看到这样一道题(稍作修改):

 function factory() {
     var name = 'laruence';
     var intro = function(){
          console.log('I am ' + name);
          console.log('I am ' + age + "years");
     }
     return intro;
}
 
function app(para){
     var name = para;
     var age = 20;
     var func = factory();
     func();
}
 
app('eve');

运行结果是:

I am laruence
脚本报错:Uncaught ReferenceError: age is not defined

虽然在不理解作用域的情况下,我也做出了正确答案。但当我看了网上大神对作用域的讲解后,再来看此题,越看越经典,当初能做对,纯粹运气。当我将此题的作用域链画出来之后,终于感觉作用域入门了。

2.图解作用域

(1)JS引擎在进入一段可执行的代码时,需要完成以下三个初始化工作:

  • 创建一个全局对象(Global Object):全局对象在创建时,将Math,String,Date,document 等常用的JS对象作为其属性。还有另外一个属性window,将window指向了自身,这样就可以通过window访问这个全局对象了。
  • 构建一个执行环境栈( Execution Context Stack):当执行一个函数时,该函数的执行环境就会被推入执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。
  • 创建一个全局执行环境(Execution Context)EC,并将这个全局执行环境EC压入执行环境栈中。
  • 创建一个与EC关联的全局变量对象(Varibale Object) VO,并把VO指向全局对象。VO中不仅包含了全局对象的原有属性,还包括在全局定义的变量和函数。同时,在定义函数A的时候,还为 A 添加了一个内部属性scope,并将scope指向了VO。每个函数在定义的时候,都会创建一个与之关联的scope属性,scope总是指向定义函数时所在的环境。
    图片描述

(2)当执行进入app('eve') 时

  • 创建函数app的执行环境EC(a):然后EC(a)推入执行环境栈的顶部并获取执行权。
  • 创建函数app的作用域链(Scope Chain):在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象。
  • 创建一个当前函数的活动对象(Activation Object) AO:AO中包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义,然后AO会被推入作用域链的顶端。需要注意的是,在定义函数B的时候,JS引擎同样也会为B添加了一个scope属性,并将scope指向了定义函数B时所在的环境,定义函数B的环境就是A的活动对象AO, 而AO位于链表的前端,由于链表具有首尾相连的特点,因此函数B的scope指向了A的整个作用域链。


    图片描述

(3)当执行进入factory() 时

  • 创建函数factory的执行环境EC(f):然后EC(f)推入执行环境栈的顶部并获取执行权。
  • 创建函数factory的作用域链Scope Chain(f):在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象。
  • 创建一个当前函数的活动对象AO(f):AO中包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义,然后AO(f)会被推入作用域链的顶端。需要注意的是,在定义函数intro的时候,JS引擎同样也会为intro添加了一个scope属性,并将scope指向了定义函数intro时所在的环境,定义函数intro的环境就是factory的活动对象AO(f), 而AO(f)位于链表的前端,由于链表具有首尾相连的特点,因此函数intro的scope指向了factory的整个作用域链。


    图片描述

(4)当执行进入func()时
函数factory被执行以后,返回了intro的引用,并赋值给了变量func,执行 func() 就相当于执行intro()。

  • 创建函数intro的执行环境EC(i),然后EC(i)推入执行环境栈的顶部并获取执行权。 此时执行环境栈中有三个执行环境,分别是全局执行环境、函数app的执行环境和函数intro,intro的执行环境在栈顶,全局执行环境在栈的底部。
  • 创建函数intro的作用域链Scope Chain(i),并初始化为函数intro的scope所包含的对象,即包含了factory的作用域链。
  • 创建函数intro的活动对象AO(i),并将intro的arguments对象和this对象作为AO(i)的属性。


    图片描述

执行func()时,它的作用域链为 VO(G ) <- AO(f) <- AO(i),所以

  • 读取name属性时,AO(i)对象上没有name属性,沿着作用域链到AO(f),读取到了name属性,值为"laruence";
  • 读取age属性时,其实你会发现只有AO(f)上定义了age属性,而AO(f)又不在作用域链上,所以此时会报脚本错误Uncaught ReferenceError: age is not defined,表示age属性未定义。这也从侧面证明了我们上面的作用域链分析是正确的。

执行完成,退出。

3.Javascript中的作用域

  • JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。
  • 在JS中,每次调用一个函数的时候 ,就会进入一个函数内的作用域,当从函数返回以后,就返回调用前的作用域。
  • 在javascript中,每个函数都有自己的执行环境,当执行一个函数时,该函数的执行环境就会被推入执行环境栈的顶部并获取执行权。当这个函数执行完毕,它的执行环境又从这个栈的顶部被删除,并把执行权并还给之前执行环境。
  • 每个函数在定义的时候,都会创建一个与之关联的scope属性,scope总是指向定义函数时所在的环境。
  • 在javascript中,每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,它的作用域链就初始化为当前运行函数的scope所包含的对象。
  • 有了作用域链, 在发生标识符解析的时候, 就会逆向查询当前scope chain列表的每一个活动对象的属性,如果找到同名的就返回。找不到,那就是这个标识符没有被定义。
  • 全局对象(Global Object) , 这个对象全局只存在一份,它的属性在任何地方都可以访问,它的存在伴随着应用程序的整个生命周期。全局对象在创建时,Math,String,Date,document 等常用的JS对象作为其属性。
  • 活动对象(Activation Object) AO,这里的活动对象扮演着变量对象的角色,只是在函数中的叫法不同而已(你可以认为变量对象是一个总的概念,而活动对象是它的一个分支), AO中包含了函数的形参、arguments对象、this对象、以及局部变量和内部函数的定义,然后AO会被推入作用域链的顶端。

参考文献

  • javascript 从定义到执行,你不知道的那些事
  • Javascript作用域原理

你可能感兴趣的:(由一道题图解JavaScript的作用域)