关于javascript:什么是词汇范围?

What is lexical scope?

有人能给我简单介绍一下词法范围吗?


我通过例子来理解它们。:)

首先,词汇范围(也称为静态范围),在C类语法中:

1
2
3
4
5
6
7
8
9
void fun()
{
    int x = 5;

    void fun2()
    {
        printf("%d", x);
    }
}

每个内部层都可以访问其外部层。

还有另一种方法,称为Lisp的第一个实现所使用的动态范围,同样,在C类语法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void fun()
{
    printf("%d", x);
}

void dummy1()
{
    int x = 5;

    fun();
}

void dummy2()
{
    int x = 10;

    fun();
}

在这里,fun既可以访问dummy1dummy2中的x,也可以访问其中声明了x的任何函数中的x

1
dummy1();

将打印5,

1
dummy2();

将打印10。

第一个称为静态,因为它可以在编译时推导出来;第二个称为动态,因为外部作用域是动态的,并且依赖于函数的链调用。

我发现静态范围更容易眼睛。大多数语言最终都是这样发展的,甚至是lisp(两者都可以,对吗?).dynamic作用域类似于将所有变量的引用传递给被调用函数。

关于编译器无法推断函数外部动态范围的示例,请考虑上一个示例,如果我们编写如下内容:

1
2
3
4
if(/* some condition */)
    dummy1();
else
    dummy2();

调用链取决于运行时条件。如果为真,则调用链如下:

1
dummy1 --> fun()

如果条件为假:

1
dummy2 --> fun()

在这两种情况下,fun的外部范围都是调用者加上调用者的调用者等。

只需提到,C语言不允许嵌套函数或动态范围。


让我们尝试尽可能短的定义:

词法作用域定义变量名在嵌套函数中的解析方式:内部函数包含父函数的作用域,即使父函数已返回。

这就是一切!


1
2
3
4
5
6
7
8
var scope ="I am global";
function whatismyscope(){
   var scope ="I am just a local";
   function func() {return scope;}
   return func;
}

whatismyscope()()

上面的代码将返回"我只是本地人"。它不会返回"我是一个全球性的"。因为函数func()计算最初定义在whatismyscope函数范围内的位置。

它不会因为被调用的对象(全局作用域/甚至来自另一个函数)而烦恼,这就是为什么全局作用域值i为global不会被打印出来的原因。

这被称为词法作用域,其中"函数是使用定义时生效的作用域链执行的"——根据javascript定义指南。

词汇范围是一个非常强大的概念。

希望这有帮助……)


范围定义了函数、变量等可用的区域。例如,变量的可用性是在其上下文中定义的,比如函数、文件或对象,它们是在中定义的。我们通常称这些局部变量。

词汇部分意味着您可以从读取源代码中派生范围。

词法范围也称为静态范围。

动态范围定义了全局变量,定义后可以从任何地方调用或引用这些变量。有时它们被称为全局变量,即使大多数编程语言中的全局变量都属于词汇范围。这意味着,它可以通过读取代码来派生,即变量在此上下文中可用。也许必须遵循uses或include子句才能找到安装或定义,但是代码/编译器知道这里的变量。

相比之下,在动态范围内,首先搜索本地函数,然后搜索调用本地函数的函数,然后搜索调用该函数的函数,依此类推,向上搜索调用堆栈。"动态"指的是变化,因为每次调用一个给定的函数时,调用堆栈可能会有所不同,因此函数可能会根据调用的位置命中不同的变量。(见此处)

要查看动态范围的有趣示例,请参见此处。

有关更多详细信息,请参阅此处和此处。

Delphi/Object Pascal中的一些示例

德尔菲有词汇量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unit Main;
uses aUnit;  // makes available all variables in interface section of aUnit

interface

  var aGlobal: string; // global in the scope of all units that use Main;
  type
    TmyClass = class
      strict private aPrivateVar: Integer; // only known by objects of this class type
                                    // lexical: within class definition,
                                    // reserved word private  
      public aPublicVar: double;    // known to everyboday that has access to a
                                    // object of this class type
    end;

implementation

  var aLocalGlobal: string; // known to all functions following
                            // the definition in this unit    

end.

最接近动态范围的delphi是registerclass()/getclass()函数对。其用途见此处。

假设调用time registerclass([tmyclass])来注册某个类,通过读取代码无法预测(它是在用户调用的button click方法中调用的),那么调用getclass("tmyclass")的代码是否会得到结果。对registerClass()的调用不必在使用getClass()的单元的词汇范围内;

动态范围的另一种可能性是Delphi2009中的匿名方法(闭包),因为它们知道调用函数的变量。它不从那里递归地遵循调用路径,因此不是完全动态的。


词汇(又称静态)作用域是指仅根据变量在代码文本语料库中的位置来确定变量的作用域。变量总是引用它的顶级环境。很好地理解它与动态范围的关系。


我喜欢像@arak这样的人提供的功能齐全、语言不可知的答案。不过,由于这个问题被标记为javascript,所以我想为这个语言编写一些非常具体的注释。

在javascript中,我们选择的范围是:

  • 原样(无范围调整)
  • 词汇var _this = this; function callback(){ console.log(_this); }
  • 绑定callback.bind(this)

我认为值得注意的是,JavaScript并没有真正的动态范围。.bind调整this关键字,这很接近,但技术上不一样。

下面是演示这两种方法的示例。每次决定如何确定回调范围时都要这样做,这样就可以应用于承诺、事件处理程序等。

词汇的

以下是您在javascript中可能称为回调的Lexical Scoping

1
2
3
4
5
6
7
8
9
10
11
12
13
var downloadManager = {
  initialize: function() {
    var _this = this; // Set up `_this` for lexical access
    $('.downloadLink').on('click', function () {
      _this.startDownload();
    });
  },
  startDownload: function(){
    this.thinking = true;
    // request the file from the server and bind more callbacks for when it returns success or failure
  }
  //...
};

范围的另一种方法是使用Function.prototype.bind

1
2
3
4
5
6
7
var downloadManager = {
  initialize: function() {
    $('.downloadLink').on('click', function () {
      this.startDownload();
    }.bind(this)); // create a function object bound to `this`
  }
//...

据我所知,这些方法在行为上是等效的。


IBM将其定义为:

The portion of a program or segment unit in which a declaration
applies. An identifier declared in a routine is known within that
routine and within all nested routines. If a nested routine declares
an item with the same name, the outer item is not available in the
nested routine.

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function x() {
    /*
    Variable 'a' is only available to function 'x' and function 'y'.
    In other words the area defined by 'x' is the lexical scope of
    variable 'a'
    */

    var a ="I am a";

    function y() {
        console.log( a )
    }
    y();

}
// outputs 'I am a'
x();

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function x() {

    var a ="I am a";

    function y() {
         /*
         If a nested routine declares an item with the same name,
         the outer item is not available in the nested routine.
         */

        var a = 'I am inner a';
        console.log( a )
    }
    y();

}
// outputs 'I am inner a'
x();

词法范围:在函数外部声明的变量是全局变量,在JavaScript程序中随处可见。函数内声明的变量具有函数作用域,并且仅对出现在该函数内的代码可见。


会话中有一个重要的部分与词汇和动态作用域相关,但却缺失了:对作用域变量的生存期的简单解释——或者当变量可以被访问时。

动态范围界定只是非常松散地与我们传统的思考方式中的"全局"范围界定相对应(我之所以提出这两者之间的比较,是因为它已经被提到了——而且我不特别喜欢链接文章的解释);最好不要在全局和动态之间进行比较。-根据链接的文章,尽管假设"作为全局范围变量的替代品,它是有用的。"

那么,在简单的英语中,这两种范围界定机制之间的重要区别是什么?

在上面的答案中,词法范围的定义非常好:词法范围的变量在定义它的函数的局部级别上是可用的,或者是可访问的。

然而,由于它不是操作的焦点,动态范围界定并没有受到太多的关注,而且它所受到的关注意味着它可能需要更多(这不是对其他答案的批评,而是一个"哦,这个答案让我们希望有更多")。所以,这里还有一点:

动态作用域意味着在函数调用的生存期内或函数执行期间,较大的程序可以访问变量。实际上,维基百科在解释两者的区别方面做得很好。为了避免混淆,以下是描述动态范围的文本:

...[I]n dynamic scoping (or dynamic scope), if a variable name's scope is a
certain function, then its scope is the time-period during which the
function is executing: while the function is running, the variable
name exists, and is bound to its variable, but after the function
returns, the variable name does not exist.


javascript中的词法范围意味着可以在变量声明之后定义的另一个函数内部访问在函数外部定义的变量。但相反,函数内部定义的变量在函数外部是不可访问的。

这个概念在JavaScript的闭包中被大量使用。

假设我们有下面的代码。

1
2
3
4
5
var x = 2;
var add = function() {
var y = 1;
return x + y;
};

现在,当您调用add()>时,它将打印3。

因此,add()函数正在访问在方法函数add之前定义的全局变量x。这是由于javascript中的词法作用域而调用的。


词法范围是指函数在定义它的上下文中查找变量,而不是在它周围的范围中立即查找变量。

如果您想了解更多细节,请看一下Lisp中词汇范围是如何工作的。KyleCronin在动态变量和词汇变量中选择的答案比这里的答案更清晰。

巧合的是,我只在一个Lisp类中了解到这一点,它也恰好应用于JS。

我在Chrome的控制台中运行了这个代码。

1
2
3
4
5
6
7
8
9
10
11
// javascript               equivalent Lisp
var x = 5;                //(setf x 5)
console.debug(x);         //(print x)
function print_x(){       //(defun print-x ()
    console.debug(x);     //    (print x)
}                         //)
(function(){              //(let  
    var x = 10;           //    ((x 10))
    console.debug(x);     //    (print x)
    print_x();            //    (print-x)
})();                     //)

输出:

1
2
3
5
10
5


在简单语言中,词汇作用域是在作用域之外定义的变量,或者在作用域内自动提供上部作用域,这意味着您不需要将其传递到那里。

前任:

1
2
3
4
5
6
7
let str="JavaScript";

const myFun = () => {
    console.log(str);
}

myFun();

//输出:javascript


我通常以身作则,这里有一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const lives = 0;

function catCircus () {
    this.lives = 1;
    const lives = 2;

    const cat1 = {
        lives: 5,
        jumps: () => {
            console.log(this.lives);
        }
    };
    cat1.jumps(); // 1
    console.log(cat1); // { lives: 5, jumps: [Function: jumps] }

    const cat2 = {
        lives: 5,
        jumps: () => {
            console.log(lives);
        }
    };
    cat2.jumps(); // 2
    console.log(cat2); // { lives: 5, jumps: [Function: jumps] }

    const cat3 = {
        lives: 5,
        jumps: () => {
            const lives = 3;
            console.log(lives);
        }
    };
    cat3.jumps(); // 3
    console.log(cat3); // { lives: 5, jumps: [Function: jumps] }

    const cat4 = {
        lives: 5,
        jumps: function () {
            console.log(lives);
        }
    };
    cat4.jumps(); // 2
    console.log(cat4); // { lives: 5, jumps: [Function: jumps] }

    const cat5 = {
        lives: 5,
        jumps: function () {
            var lives = 4;
            console.log(lives);
        }
    };
    cat5.jumps(); // 4
    console.log(cat5); // { lives: 5, jumps: [Function: jumps] }

    const cat6 = {
        lives: 5,
        jumps: function () {
            console.log(this.lives);
        }
    };
    cat6.jumps(); // 5
    console.log(cat6); // { lives: 5, jumps: [Function: jumps] }

    const cat7 = {
        lives: 5,
        jumps: function thrownOutOfWindow () {
            console.log(this.lives);
        }
    };
    cat7.jumps(); // 5
    console.log(cat7); // { lives: 5, jumps: [Function: thrownOutOfWindow] }
}

catCircus();

Lexical Scope means that in a nested group of functions, the inner
functions have access to the variables and other resources of their
parent scope. This means that the child functions are lexically bound
to the execution context of their parents. Lexical scope is sometimes
also referred to as Static Scope.

1
2
3
4
5
6
7
8
9
10
11
12
13
function grandfather() {
    var name = 'Hammad';
    // likes is not accessible here
    function parent() {
        // name is accessible here
        // likes is not accessible here
        function child() {
            // Innermost level of the scope chain
            // name is also accessible here
            var likes = 'Coding';
        }
    }
}

The thing you will notice about lexical scope is that it works
forward, meaning name can be accessed by its children's execution
contexts. But it doesn't work backward to its parents, meaning that
the variable likes cannot be accessed by its parents. This also tells
us that variables having the same name in different execution contexts
gain precedence from top to bottom of the execution stack. A variable,
having a name similar to another variable, in the innermost function
(topmost context of the execution stack) will have higher precedence.

注意这是从这里取的


对于这个问题,我们可以从另一个角度来理解,即后退一步,看看范围界定在更大的解释框架(运行一个程序)中的作用。换句话说,假设您正在为一种语言构建一个解释器(或编译器),并负责计算输出,给定一个程序和一些输入。

口译包括三个方面:

1)状态-即堆和堆栈上的变量和引用内存位置。

2)在该状态下的操作-即程序中的每一行代码

3)给定操作运行的环境——即状态对操作的投影。

解释器从程序中的第一行代码开始,计算其环境,在该环境中运行该行,并捕获其对程序状态的影响。然后它跟随程序的控制流执行下一行代码,并重复该过程直到程序结束。

计算任何操作的环境的方法是通过编程语言定义的一组正式规则。术语"绑定"经常用于描述程序整体状态到环境中某个值的映射。请注意,"整体状态"不是指全局状态,而是指在执行过程中的任何点上每个可到达定义的总和)

这是定义范围问题的框架。接下来我们的选择是什么。

  • 作为解释器的实现者,您可以通过使环境尽可能接近程序的状态来简化任务。因此,一行代码的环境将简单地由前一行代码的环境定义,并应用该操作的效果,而不管前一行是赋值、函数调用、函数返回还是while循环等控制结构。

这是动态作用域的要点,其中运行任何代码的环境都绑定到由其执行上下文定义的程序状态。

  • 或者,您可以考虑使用您的语言的程序员,并简化他或她的任务,以跟踪变量可以接受的值。在对结果(过去执行的全部)进行推理时,有太多的路径和太多的复杂性。词法作用域通过将当前环境限制在当前块、函数或其他作用域单元及其父级(即封闭当前时钟的块或调用当前函数的函数)中定义的状态部分来帮助实现这一点。

换句话说,对于词汇范围,任何代码看到的环境都绑定到与语言中显式定义的范围(如块或函数)相关联的状态。