关闭

JavaScript:执行环境及作用域链

标签: javascriptweb前端执行环境作用域链执行上下文
385人阅读 评论(0) 收藏 举报
分类:

1、执行环境

       定义了变量或函数有权访问的其它数据,决定了它们的各自行为。每个执行环境都有一个与之关联的变量对象(variable object, VO),执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。

(1)、执行环境(执行上下文)

     全局执行环境是最外层的一个执行环境,在web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

实例:

console.log('EC0');
function funcEC1() {
   console.log('EC1');
   var funcEC2 = function() {
     console.log('EC2');
     var funcEC3 = function() {
       console.log('EC3');
     };
     funcEC3();
   }
  funcEC2();
}
funcEC1();
// EC0 EC1 EC2 EC3
上面实例的执行环境为:



(2)、变量对象

  变量对象(Variable Object, 缩写为VO)是一个抽象概念中的“对象”,它用于存储执行上下文中的:变量、函数声明和函数参数。

activeExecutionContext = {
   VO:{
       data_var,
       data_func_declaration,
       data_func_arguments
   }
};
GlobalContextVO (VO === this === global)
例如:

var a = 10;
function test(x) {
  var b = 20;
}
test(30);
上面实例的变量对象为:

//全局执行环境下的VO
VO(globalContext) = {
     a : 10,
     test : <ref to function>
};
//函数执行环境VO
VO(test functionContext) = {
     x : 30,
     b: 20
};

在全局执行环境,Global对象可以说是ECMAScript中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。事实上,没有全局变量或全局函数,所有在全局作用域中定义的属性和函数,都是Global对象的属性。
VO(globalContext) === [[global]];
[[global]] = {
   Math : <...>,
   String : <...>,
   isNaN : function() {[Native Code]}
   ...
   ...
   window : global // applied by browser(host)
};
GlobalContextVO (VO === this === global)
String(10); //[[global]].String(10);
window.a = 10; // [[global]].window.a = 10
this.b = 20; // [[global]].b = 20;
函数中的变量对象等于函数的激活对象

VO(functionContext) === AO;
AO = {
  arguments : <Arg0>
};
arguments = {
  callee,
  length,
  properties-indexes
};
在函数的变量初始化阶段:VO按照如下顺序填充

1).函数参数(若未传入,初始化该参数值为undefined)
2).函数声明(若发生命名冲突,会覆盖)
3).变量声明(初始化变量值为undefined,若发生命名冲突,会忽略。)

实例1:

function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
  b = 20;
}
test(10);
变量初始化阶段的VO==AO:

AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: <ref to func "d">
  e: undefined
};
注意:函数表达式不会影响VO

代码执行阶段的VO:

AO(test) = {
   a: 10,
   b: 20,
   c: 10,
   d: <reference to FunctionDeclaration "d">
   e: function _e() {};
};

实例2:

alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {}
alert(x); // 20
if (true) {
   var a = 1;
} else {
   var b = true;
}
alert(a); // 1
alert(b); // undefined


2、作用域

   作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和局部作用域。

全局作用域:在代码中任何地方都能访问到的对象拥有全局作用域。全局作用域的变量是全局对象的属性,不论在什么函数中都可以直接访问,而不需要通过全局对象,但加上全局对象,可以提供搜索效率。

   a.没有用var声明的变量(除去函数的参数)都具有全局作用域,成为全局变量,所以声明局部变量必须要用var。

   b.window的所有属性都具有全局作用域

   c.最外层函数体外声明的变量也具有全局作用域

局部作用域:局部变量的优先级高于全局变量。

   a.函数体内用var声明的变量具有局部作用域,成为局部变量

   b.函数的参数也具有局部作用域

  JavaScript是函数作用域(function scope),没有块级作用域。无论函数体内的变量在什么地方声明,对整个函数都是可见的,即JavaScript函数里声明的所有变量都被提前到函数体的顶部,只是提前变量声明,变量的赋值还是保留在原位置。

  单纯的JavaScript作用域还是很好理解的,在一些类C编程语言中花括号内的每一段代码都有各自的作用域,而且变量在声明它们的代码段外是不可见的,称之为块级的作用域,JavaScript容易让初学者误会的地方也在于此,JavaScript并没有块及的作用域,只有函数级作用域:变量在声明它们的函数体及其子函数内是可见的。

  变量没有在函数内声明或者声明的时候没有带var就是全局变量,拥有全局作用域,window对象的所有属性拥有全局作用域;在代码任何地方都可以访问,函数内部声明并且以var修饰的变量就是局部变量,只能在函数体内使用,函数的参数虽然没有使用var但仍然是局部变量。        

            var a=3; //全局变量
            function fn(b){ //局部变量
                c=2; //全局变量
                var d=5; //局部变量
                function subFn(){
                    var e=d; //父函数的局部变量对子函数可见
                    for(var i=0;i<3;i++){
                        console.write(i);
                    }
                    alert(i);//3, 在for循环内声明,循环外function内仍然可
                    见,没有块作用域
                }
            }
            alert(c); //在function内声明但不带var修饰,仍然是全局变量

  只要是理解了JavaScript没有块作用域,简单的JavaScript作用域很好理解,还有一点儿容易让初学者迷惑的地方是JavaScript变量可函数的与解析或者声明提前,好多种叫法但说的是一件事情,JavaScript虽然是解释执行,但也不是按部就班逐句解释执行的,在真正解释执行之前,JavaScript解释器会预解析代码,将变量、函数声明部分提前解释,这就意味着我们可以在function声明语句之前调用function,这多数人习以为常,但是对于变量的与解析乍一看会很奇怪          

            console.log(a); //undefined
            var a=3;
            console.log(a); //3
            console.log(b); //Uncaught ReferenceError: b is not defined

上面代码在执行前var a=3; 的声明部分就已经得到预解析(但是不会执行赋值语句),所以第一次的时候会是undefined而不会报错,执行过赋值语句后会得到3,上段代码去掉最后一句和下面代码是一样的效果。          

            var a;
            console.log(a); //undefined
            a=3;
            console.log(a); //3

  然而如果只是这样那么JavaScript作用域问题就很简单了,然而由于函数子函数导致的问题使作用域不止这样简单。大人物登场——执行环境或者说运行期上下文(好土鳖):执行环境(execution context)定义了变量或函数有权访问的其它数据,决定了它们的各自行为。每个执行环境都有一个与之关联的变量对象(variable object, VO),执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。

  全局执行环境是最外层的一个执行环境,在web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和放大创建的。每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

3、作用域链

  当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain,不简称sc)来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)。例如定义下面这样一个函数:   

function add(num1,num2) {
    var sum = num1 + num2;
    return sum;
 }

  在函数add创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量,如下图所示(注意:图片只例举了全部变量中的一部分):

 

 

  函数add的作用域将会在执行时用到。例如执行如下代码:

          var total = add(5,10);

  执行此函数时会创建一个称为“执行环境(execution context)”的内部对象,执行环境定义了函数执行时的环境。每个执行环境都有自己的作用域链,用于标识符解析,当执行环境被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

  这些值按照它们出现在函数中的顺序被复制到执行环境的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当执行环境被销毁,活动对象也随之销毁。新的作用域链如下图所示:

 

 

 

  在函数执行过程中,没遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。函数执行过程中,每个标识符都要经历这样的搜索过程。

  总结为:当函数执行时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[scope])。然后使用this、arguments和其他命名参数的值来初始化函数的活动对象。

(4)、作用域链和代码优化

  从作用域链的结构可以看出,在执行环境的作用域链中,标识符所在的位置越深,读写速度就会越慢。如上图所示,因为全局变量总是存在于执行环境作用域链的最末端,因此在标识符解析的时候,查找全局变量是最慢的。所以,在编写代码的时候应尽量少使用全局变量,尽可能使用局部变量。一个好的经验法则是:如果一个跨作用域的对象被引用了一次以上,则先把它存储到局部变量里再使用。例如下面的代码:  

     function changeColor(){
        document.getElementById("btnChange").onclick=function(){
        document.getElementById("targetCanvas").style.backgroundColor="red";
       };
     }

 这个函数引用了两次全局变量document,查找该变量必须遍历整个作用域链,直到最后在全局对象中才能找到。这段代码可以重写如下:

   function changeColor(){
    var doc=document;
    doc.getElementById("btnChange").onclick=function(){
        doc.getElementById("targetCanvas").style.backgroundColor="red";
    };
   }

这段代码比较简单,重写后不会显示出巨大的性能提升,但是如果程序中有大量的全局变量被从反复访问,那么重写后的代码性能会有显著改善。

(5)、改变作用域链

  函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文,当函数执行完毕,执行上下文会被销毁。每一个运行期上下文都和一个作用域链关联。一般情况下,在运行期上下文运行的过程中,其作用域链只会被 with 语句和 catch 语句影响。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:618240次
    • 积分:8514
    • 等级:
    • 排名:第2359名
    • 原创:295篇
    • 转载:29篇
    • 译文:0篇
    • 评论:179条
    博客专栏
    文章分类
    最新评论