使用词法环境跟踪变量的作用域

词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。例如,查看如下代码:

var ninja = "Hattori";

console.log(ninja);

当console.log语句访问ninja变量时,会进行词法环境的查询。

注意:词法环境是JavaScript作用域的内部实现机制,人们通常称为作用域(scopes)。

通常来说,词法环境与特定的JavaScript代码结构关联,既可以是一个函数、一段代码片段,也可以是try-catch语句。这些代码结构(函数、代码片段、try-catch)可以具有独立的标识符映射表。

注意: 在JavaScript的ES6初版中,词法环境只能与函数关联。变量只存在于函数作用域中。这也带来了一些混淆。因为JavaScript是一门类C的语言,从其他类C语言(如C++、C#、Java等)转向JavaScript的开发者通常会预期一些初级概念。例如块级作用域。在ES6中最终修复了块级作用域问题。

代码嵌套

语法环境主要基于代码嵌套,通过代码嵌套可以实现代码结构包含另一代码结构。以下代码显示了多种代码嵌套类型。

console.log("-------------------代码嵌套--------------");
var ninjaTest = 'Muneyoshi';
//skulk函数包含在全局代码中
function skulk() {
  var action = "skulking";
//report函数嵌套在skulk函数中
  function report() {
    var reportNum = 3;
   //for循环嵌入在report函数中
    for (var i = 0; i < reportNum; i++) {
      console.log(ninjaTest + " " + action + " " + i);
    }
  }
  report();
}
skulk();

 

从上述代码中可以看出:

1.for循环嵌套在report函数中。

2.report函数嵌套在skulk函数中。

3.skulk函数嵌套在全局代码中。

 

在作用域范围内,每次执行代码时,代码结构都获得与之关联的语法环境。例如,每次调用skulk函数,都将创建新的函数词法环境。

此外,需要强调的是,内部代码结构可以访问外部代码结构中定义的变量。例如,for循环可以访问report函数、skulk函数以及全局变量代码中的变量;report函数可以访问skulk函数及全局代码中的变量;skulk函数可以访问的额外变量但仅是全局代码中的变量。

JavaScript引擎是如何跟踪这些变量的呢?如何判断可访问性呢?这就是词法环境的作用?

代码嵌套与词法环境

除了跟踪局部变量、函数声明、函数的参数和词法环境外,还有必要跟踪外部(父级)词法环境。因为我们需要访问外部代码结构中的变量,如果在当前环境中无法找到某一标识符,就会对外部环境进行查找。一旦查找到匹配的变量,或是在全局环境中仍然无法查找到对应的标识符而返回错误,就会停止查找。

 

console.log("--------------代码嵌套与词法环境-------------");
var ninjaTest = 'Muneyoshi';
function skulk() {
  var action = "skulking";
  function report() {
    var intro = "Aha!";
    if (intro === 'Aha!') {
      console.log("Local");
    }
    if (action === 'skulking') {
      console.log("Outer!");
    }
    if (ninjaTest === 'Muneyoshi') {
      console.log("Global!");
    }
  }
  report();
}
skulk();

 

下图中展示:1.当执行report函数时,标识符intro、action以及ninja是如何查找的。

2.在全局调用skulk函数,skulk函数又调用report函数。每个执行上下文都有一个与之关联的词法环境,词法环境中包含了在上下文中定义的标识符的映射表,例如,全局环境中具有ninja与skulk的映射表,skulk环境中具有action与report的映射表,report环境中具有intro的映射表。

JavaScript引擎如何查找变量的值:

 

查找intro变量

在report环境中检查——>找到

查找action变量

1.在report环境中检查——>未找到

2.检查report的外层环境skulk,在skulk环境中检查——>找到

查找ninjaTest变量

1.在report环境中检查——>未找到

2.检查report的外层环境skulk,在skulk环境中检查——>未找到

3.检查skulk的外层环境global,在global环境中检查——>找到

在特定的执行上下文中,我们的程序不仅直接访问词法环境中定义的局部变量,而且还会访问外部环境中定义的变量。例如,report函数体访问了在skulk函数中定义的action变量,也访问了全局的ninjaTest变量。为了实现这一点,我们需要跟踪这些外部环境。JavaScript实现这一点得益于函数是第一型对象的特性。

无论何时创建函数,都会创建一个与之相关联的词法环境,并存储在名为[[Environment]]的内部属性上(也就是说无法直接访问或操作)。两个中括号用于标志内部属性。

在上面的图中,skulk函数保存全局环境的引用,report函数保存skulk环境的引用,这些都是函数被创建时所在的环境。

这些环境是在函数创建时决定的,因此除了全局环境外,report函数还可以访问shulk环境。

注意:

乍看之下会觉得奇怪。为什么不直接跟踪整个执行上下文,直接搜索与环境匹配的标识符映射表呢?从技术上来说,这是可行的。但是,需要记住的是,javaScript函数可以作为任意对象进行传递,定义函数时的环境与调用函数的环境往往是不同的(比如闭包)。

无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此时,还会创建一个与之相关联的词法环境。现在来看最重要的部分:外部环境与新建的词法环境,JavaScript引擎将调用函数的内置[[Enviiroment]]属性与创建函数时的环境进行关联。

在上图中,调用skulk函数时,外部环境与新建的skulk环境成为全局环境(因为这是创建skulk函数时的环境)。类似,当调用report函数时,外部环境与新创建的report环境都被置入skulk的环境中。

下面看一下report函数:

function report() {

    var intro = "Aha!";

    if (intro === 'Aha!') {

        console.log("Local");

    }

    if (action === 'skulking') {

        console.log("Outer!");

    }

    if (ninjaTest === 'Muneyoshi') {

        console.log("Global!");

    }

    if (hhhh === 'aa') {

        console.log("aaaa");

    }

}

执行第一句if语句时,首先需要查找intro标识符的值。JavaScript引擎首先检查当前执行上下文,即report函数环境。由于report环境包含一个intro变量的引用,因此intro标识符就查找完成。

第二句if语句需要查找action标识符。又一次需要检查当前环境的执行上下文。但是report环境里没有action标识符的引用,因此JavaScript引擎需要查找report的外部环境:skulk环境。幸运的是,skulk环境包含有action标识符的引用,action标识符就查找完成。查找ninjaTest标识符的处理过程也是类似的(小提示:可在全局环境中查找到ninjaTest标识符)。

参考《JavaScript忍者秘籍》

猜你喜欢

转载自blog.csdn.net/zhangying1994/article/details/85237812