(转载)浅谈JavaScript的闭包和作用域链

闭包和作用域链是JavaScript中比较重要的概念,这两天翻阅了一些资料,总结了一下。首先,看看几段简单的代码。

代码1:

   var  name  =   " stephenchan " ;
   var  age  =   23 ;
  function  myFunc() {
       alert(name);
        var  name  =   " endlesscode " ;
       alert(name);
       alert(age);
       alert(weight);
   }
  myFunc();
  myFunc();

上述代码1中,两次调用myFunc()的输出是一致的。可能你会认为输出是:

  stephenchan
  endlesscode
  23
  [Reference Error]

 

但是结果却是:

  代码1输出:
  undefined
  endlesscode
  23
  [Reference Error]

 

代码2:

   var  i  =   10 ;
   function  myFunc() {
        var  i  =   20 ;
        function  innerFunc() {
           alert(i);
       }
        return  innerFunc;
   }
   var  func  =  myFunc();
  func();

 

上面的代码2会输出20,但为什么不输出10或者是输出undefined?

代码3:

   var  name  =   " stephenchan " ;
   function  callMePlz() {
       alert(name);
   }
  
   function  myFunc() {
        var  name  =   " endlesscode " ;
       callMePlz();
   }
 
  myFunc();

 

上面的代码3输出的会是endlesscode、stephenchan还是undefined?

  代码3输出:
  stephenchan

代码4:

   function  callMePlz() {
        var  name  =   " stephenchan " ;
        var  intro  =   function () {
           alert( " I am  "   +  name);
       }
        return  intro;
   }
  
   function  showMe(arg) {
       var  name  =  arg;
       var  func  =  callMePlz();
      func();
  }
  showMe( " endlesscode " );

 

上面的代码4与代码3不同的是,从callMePlz返回的函数引用,然后再执行函数。

  代码4输出:
  I am stephenchan

 

代码5:

   var  name  =   " stephenchan " ;
   function  callMePlz() {
        var  intro  =   function () {
           alert( " I am  "   +  name);
       }
        return  intro;
   }
  
   function  showMe(arg) {
       var  name  =  arg;
       var  func  =  callMePlz();
      func();
  }
  showMe( " endlesscode " );

 

上面的代码5与代码4不同的是原来在callMePlz函数中的变量name在全局环境中声明了,但输出的结果是:

  代码5输出:
  I am stephenchan

  

先不对上面的代码进行说明,讲述一下闭包和作用域链的概念。

闭包(closure)是什么?闭包与函数有着紧密的关系。“在JavaScript中,一个函数只是一段静态的代码、脚本文件,因此函数是一个代码书写时,以及编译期的、静态的概念;而闭包是函数的代码在运行过程中的一个动态环境,是一个运行期的、动态的概念”。这是《JavaScript语言精髓和编程实践》中对函数和闭包的描述,实际上我们常说的闭包倒是可以表现为如上面代码2中的innerFunc一样,在myFunc的函数执行后返回的是一个在其内部定义的、外部可调用的函数引用,这个函数语言的特性在C和C++是没有的。为什么在myFunc结束之后innerFunc还能正常访问到myFunc里面的数据呢?这就涉及到函数执行环境与闭包的相关概念,闭包中所保留着函数运行的实例,环境以及作用域链等等,并在myFunc调用之后没有将函数实例直接丢弃,因此在调用innerFunc的时候能够引用到myFunc中声明的i。

作用域链(scope chain)是什么?顾名思义,就是由作用域组成的链,是一个类似链状的数据结构。作用域就是对上下文环境的数据描述。闭包和作用域链是紧密关系的,函数实例执行时的闭包是构成作用域链的基本元素。JavaScript代码在执行前会进行语法分析,在语法分析阶段,会记录全局环境中的变量声明和函数定义,构造函数的调用对象(Call Oject、Activation Object、Activate Object、活动对象,不同称呼罢了)和在全局环境下的作用域链。

(转载)浅谈JavaScript的闭包和作用域链_第1张图片

 

 

图1

图1是《JavaScript语言精髓和编程实践》一书中对闭包相关元素的内部数据结构的描述。我们主要关注其中的ScriptObject,ScriptObject是对调用对象的一种描述。ScriptObject在语法分析阶段就已经构造好了,其中的varDecls是保存着函数中的变量声明,funcDecls保存着内部的函数声明。

在语法分析阶段,varDecls保存在函数中用var进行显示声明的局部变量,并且置默认值为undefined,这里就是在代码1中"alert(name)"输出为undefined的原因,由于myFunc在语法分析阶段就已经保留了标记符name在varDecls,在赋值语句"var name='endlesscode'"执行之前name的值都是undefined,因此在"alert(name)"的时候就显示为undefined了。

而函数定义在语法分析阶段工作就稍微有点不同。在语法分析阶段,发现有函数定义的时候,除了记录函数的声明外,还会创建一个函数对象,并将当前的作用域链赋值给此函数对象的[[scope]]属性(这个属性是JavaScript引擎内部维护的,但是Firefox却是可以通过私有属性__parent__来访问它),这里要注意的是在语法分析阶段将作用域链赋值给[[scope]]属性,而不是在执行阶段。如果是在全局环境下,但当前的作用域链为只有一个元素,就是全局的调用对象(Windows Object, Global Object,不同称呼罢了)。这就是为什么在《JavaScript权威指南》中提到“JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。”

下面以一段代码的大概处理流程来进行说明:

  var  outerVar1  =   " var in global code " ;
  function  outerFunc(arg1, arg2) {
       var  innerVar1  =   " var in function code " ;
       function  innerFunc() {  return  outerVar1  +   " - "   +  innerVar1  +   " - "   +  (arg1  +  arg2); }
       return  innerFunc();
  }
  var  outerVar2  =  outerFunc( 10 20 );

 

执行处理过程大致如下:

  1. 引擎启动,初始化Global Object,即window对象,全局的调用对象,建立作用域链,假设为scope_1,作用域链中只包含全局的上下文环境,即Global Object。
  2. 语法分析阶段,扫描JavaScript代码,获取代码中变量和函数定义,其扫描过程如下:
    1. 发现变量outerVar1,在Global Object的varDecls中添加outerVar1属性,值为undefined。
    2. 发现函数outerFunc的定义,在Global Object的funcDecls中添加outerFunc,并创建函数对象(这里应该创建的是函数的原型对象),将scope_1传递给outerFunc的函数对象,即outerFunc内部的[[scope]]属性。另外注意,创建过程并不会对函数体中的JavaScript代码做特殊处理,可以理解为只是将函数JavaScript代码保存中函数对象的内部属性上,在函数执行时再做进一步处理。也就是说,这一步大概处理的就是记录函数定义,赋值[[scope]]属性记录当前定义的作用域,而没有进一步对outerFunc里面的代码进行进一步的语法分析。
    3. 发现变量outerVar2,在Global Object中的varDecls中添加属性,值为undefined。
  3. 全局环境下语法分析结束,执行outerVar1赋值语句,赋值为"var in global code"。
  4. 执行outerFunc,获取返回值。
    1. 创建调用对象,假设为avt_obj_1。同时将avt_obj_1链接起outerFunc的[[scope]]属性,构成一个新的作用域链,假设为scope_2,scope_2中的第一个对象为act_obj_1,act_obj_1指向scope_1。
    2. 处理参数列表,在act_obj_1中设置属性arg1、arg2,值分别为10和20。创建arguments对象并进行设置,将arguments设置为act_obj_1的属性。
    3. 对outerFunc函数体进行语法分析,注意这里在全局语法分析的时候并没有对outerFunc函数体进行语法分析:
      1. 发现变量innerVar1,在act_obj_1中的varDecls添加innerVar1属性,值为undefined。
      2. < 发现函数innerFunc的定义,使用这个定义创建函数对象,并将scope_2传递给innerFunc,作为innerFunc的[[scope]]属性,在act_obj_1的funcDecls添加innerFunc。
    4. outerFunc函数语法分析结束,执行innerVar1赋值语句,赋值为"var in function code"。
    5. 执行innerFunc,执行函数的处理流程是一致的:
      1. 创建调用对象,假设为act_obj_2;同时将avt_obj_2链接起innerFunc[[scope]]属性,构成一个新的作用域链,假设为scope_3,scope_3中的第一个对象为act_obj_2,act_obj_2指向scope_2。
      2. 处理参数列表,因为innerFunc没有参数,所以只需要创建arguments对象并设置为act_obj_2的属性。
      3. 对innerFunc进行语法分析,识别变量和函数,但没有发现变量定义和函数声明。
      4. 执行innerFunc函数。对任何一个变量引用,从scope_3开始进行链式搜索,以scope_3->scope_2->scope_1的顺序进行搜索,结果发现outerVar1在scope_1中的Global Object发现;innerVar1、arg1、arg2在scope_2中的act_obj_1中找到。
      5. 检查scope_3和act_obj_2的引用,发现没有其他引用,则丢弃,让引擎进行垃圾回收。
      6. 返回innerFunc执行的值。
    6. 检查没有对act_obj_1和scope_2的引用,则丢弃act_obj_1和scope_2。
    7. 返回结果。
  5. 将outerFunc的返回结果赋值给outerVar2。

我们拿代码4的例子再对作用域链进行简单的分析:
代码4:

   function  callMePlz() {
        var  name  =   " stephenchan " ;
        var  intro  =   function () {
           alert( " I am  "   +  name);
       }
        return  intro;
   }
  
   function  showMe(arg) {
       var  name  =  arg;
       var  func  =  callMePlz();
      func();
  }
  showMe( " endlesscode " );

 假如全局的语法分析已经结束,已经开始执行"showMe('endlesscode')"了。在进入执行showMe的执行上下文时,我们可以看到"showMe"函数中[[scope]]属性记录中的作用域链为:

1  [[scope]]  =  [
2      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
3      document : ...,
4      location : ...
5      }
6  ]

创建showMe的调用对象后,则新的作用域链为showMe的调用对象和全局调用对象组成:

 1  [scope chain]  =  [
 2      { // showme_activation_obj
 3          name : undefined,
 4          func : undefined,
 5          arg :  " endlesscode " ,
 6          arguments : ...
 7       },
 8      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
 9      document : ...,
10      location : ...
11      }
12  ]

也就是"showMe调用对象->Global调用对象"这样的链式关系。接着,忽略showMe函数中语法分析等过程,到执行callMePlz()函数时,callMePlz函数的[[scope]]属性为

1  [[scope]]  =  [
2      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
3      document : ...,
4      location : ...
5      }
6  ]

 

callMePlz函数的[[scope]]属性指示的作用域链也只包括了全局调用对象,因为callMePlz也是在全局环境下定义的。创建callMePlz的调用对象后,则新的作用域链为callMePlz的调用对象和全局调用对象组成:

 1  [scope chain]  =  [
 2      { // callmeplz_activation_obj
 3          name : undefined,
 4          intro : undefined,
 5          arguments : ...
 6       },
 7      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
 8      document : ...,
 9      location : ...
10      }
11  ]

也就是"callMePlz调用对象->Global调用对象"这样的链式关系。可以看到,在callMePlz的作用域链中,并没有包括showMe的调用对象。当callMePlz进行语法分析的时候,找到intro函数时,将intro函数的[[scope]]属性赋值为:

 1  [scope chain]  =  [
 2      { // callmeplz_activation_obj
 3          name : undefined,    // 这里还是undefined,当语法分析结束,执行callMePlz时,这里就赋值为"stephenchan"
 4          intro : undefined,
 5          arguments : ...
 6       },
 7      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
 8      document : ...,
 9      location : ...
10      }
11  ]

callMePlz返回的是intro函数对象的引用,当在showMe函数中执行intro函数时,创建intro函数的调用对象,此时intro函数的作用域链为:

 1  [scope chain]  =  [
 2      { // intro_activation_obj
 3          arguments : ...
 4       },
 5      { // callmeplz_activation_obj
 6          name : undefined,    // 这里还是undefined,当语法分析结束,执行callMePlz时,这里就赋值为"stephenchan"
 7          intro : undefined,
 8          arguments : ...
 9       },
10      { // Global Object,因为在全局没有var声明的变量,因此就没有列出来
11      document : ...,
12      location : ...
13      }
14  ]

由于在intro函数中没有声明变量和函数,所以看到的也只是一些内置的属性成员,此时intro函数的作用域链则为:"intro调用对象->callMePlz调用对象->Global调用对象",因此,当执行intro函数时,则以"intro调用对象->callMePlz调用对象->Global调用对象"的顺序去搜索"name"变量,发现在callMePlz调用对象上找到了,因此在代码4中输出的是"I am stephenchan"而不是"I am endlesscode"。

以上面的分析方法来分析上述的其他代码,就容易理解其输出了。

 

对于Eval函数的环境中,进入Eval函数执行时会创建一个新的作用域链,至于作用域链的内容,在IE和Mozilla中会稍有不同。

var  i  =   100 ;
function  myFunc(ctx) {
    
var  i  =   " test " ;
    eval(
' i = "hello world!" ' );
}
myFunc();
alert(i);

 

上面的代码,毫无疑问,在IE和Mozilla都会显示100,因为eval代码执行的上下文环境是myFunc函数中的i,而非全局中的i。然后,再看下面代码:

var  i  =   100 ;
function  myFunc(ctx) {
    
var  i  =   " test " ;
    window.eval(
' i = "hello world!" ' );
}
myFunc();
alert(i);

上面代码中,输出的还是100吗?还是"hello world!"呢?这取决于不同的浏览器,在IE中,输出的結果依然100,而在Mozilla中,输出却为"hello world!",因为在IE中,eval函数作为全局对象Global Object的函数,只能获得访问到其执行环境下的作用域链,而不能获得全局作用域链的能力。而在Mozilla中,eval函数是所有对象的一个方法,而并非只是Global Object的方法,因此以window.eval的方式调用,得到的作用域链则是全局环境下的作用链,因此直接将全局环境下的i进行了修改。
另外,函数闭包内的标识符系统有优先顺序,其优先级从高到低为:this > 局部变量(varDecls) > 函数形式参数名(argsName) > arguments关键字 > 函数名(funcNames)。

// 输出'hi',说明argsName > funcNames。
function  foo(foo) {
    alert(foo);
}
foo(
' hi ' );

// 输出100的类型"number",说明argsName > arguments。
function  foo2(arguments) {
    alert(
typeof  arguments);
}
foo2(
100 );

// 输出arguments的类型为'object‘,说明arguments > funcNames。
function  arguments() {
    alert(
typeof  arguments);
}
arguments();

// 输出'test',形式参数名与未赋值局部变量重复时,取形式参数值。
function  foo3(str) {
    
var  str;
    alert(str);
}
foo3(
' test ' );

// 输出'member',形式参数与有值的局部变量重复时,取局部变量。
function  foo4(str) {
    
var  str  =   ' member ' ;
    alert(str);
}
foo4(
' test ' );

 

 

 

原文来自:http://blog.endlesscode.com/2010/01/20/javascript-closure-scope-chain/

转载于:https://www.cnblogs.com/fengmiaosen/archive/2011/01/09/1931538.html

你可能感兴趣的:((转载)浅谈JavaScript的闭包和作用域链)