什么是词汇范围?


681

什么是词汇作用域简介?


88
乔尔(Joel)在播客58中鼓励这样的问题,因为他希望SO成为解答的地方,即使在其他地方也回答了这些问题。即使有人可以礼貌一点,这也是一个有效的问题。
拉尔夫·M·里肯巴赫

5
@rahul我知道这是一个老问题。但是我敢肯定,即使在2009年,SO也希望要求者做出一些基本的努力来解决它。就目前而言,它根本不显示任何努力。可能是,这就是为什么它被许多人否决的原因?
PP

13
提出这个问题时,提问者可能不会(或不太)英语流利
Martin

27
问题很客气,他只是说他想要什么。您可以自由回答。这里不需要过分的礼貌。
Markus Siebeneicher 2014年

25
我认为这样的问题很好,因为它可以为SO构建内容。IMO,谁在乎这个问题是否在努力……答案将具有丰富的内容,这就是此留言板上的重要内容。
Jwan622

Answers:


685

我通过示例了解它们。:)

首先,采用类似C的语法的词汇作用域(也称为静态作用域):

void fun()
{
    int x = 5;

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

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

Lisp的第一个实现使用另一种称为动态范围的方式,再次使用类似于C的语法:

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

void dummy1()
{
    int x = 5;

    fun();
}

void dummy2()
{
    int x = 10;

    fun();
}

在这里fun既可以访问xdummy1dummy2,或x在调用任何函数funx在其声明。

dummy1();

将打印5

dummy2();

将打印10。

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

我发现静态范围界定对眼睛来说更容易。最终,大多数语言都采用了这种方式,甚至Lisp也是如此(对吗?)。动态作用域就像将所有变量的引用传递给调用的函数一样。

作为为什么编译器无法推断函数外部动态范围的示例,请考虑我们的最后一个示例。如果我们这样写:

if(/* some condition */)
    dummy1();
else
    dummy2();

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

dummy1 --> fun()

如果条件为假:

dummy2 --> fun()

fun两种情况的外部范围都是调用方加上调用方的调用方,依此类推

只需提及C语言既不允许嵌套函数也不允许动态作用域。


18
我还要指出一个我刚刚发现的非常非常容易理解的教程。Arak的例子很好,但是对于需要更多例子的人来说可能太短了(实际上,与其他语言相比。)。看一看。了解这一点很重要,因为该关键字将使我们理解词汇范围。howtonode.org/what-is-this
CppLearner 2011年

9
这是一个很好的答案。但问题标有JavaScript。因此,我认为这不应标记为已接受的答案。JS中的词汇范围有所不同
博扬

6
极好的答案。谢谢。@Bo阳我不同意。我不是Lisp编码员,但是发现Lisp示例很有帮助,因为它是动态作用域的示例,您在JS中无法找到它。
dudewad

4
最初,我认为示例是有效的C代码,并且对C中是否存在动态作用域感到困惑。也许最后的免责声明可以移到代码示例之前?
Yangshun Tay

2
这仍然是一个非常有用的答案,但我认为@Boyang是正确的。这个答案指的是“级别”,它更像是C的块作用域。JavaScript默认情况下没有块级作用域,因此在for循环内是典型的问题。除非使用ES6 let或JavaScript,否则JavaScript的词汇范围仅在功能级别上const
icc97 '18

274

让我们尝试最短的定义:

词法作用域定义了如何在嵌套函数中解析变量名:内部函数包含父函数的范围,即使父函数已经返回

这就是全部!


21
最后一部分:“即使父函数已返回”也被称为Closure。
Juanma Menendez

1
只需一句就可以理解词法界定和关闭。谢谢!!
地牢

63
var scope = "I am global";
function whatismyscope(){
   var scope = "I am just a local";
   function func() {return scope;}
   return func;
}

whatismyscope()()

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

它不会被调用的内容所困扰(即使是全局作用域/甚至在另一个函数中),这就是为什么不会打印我为全局范围的全局作用域值的原因。

这称为词法作用域,其中“ 根据JavaScript定义指南,使用定义时有效的作用域链执行功能 ”。

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

希望这可以帮助..:)


3
这是一个很好的解释,如果您编写函数func(){return this.scope;},我想再添加一件事,那么只要使用此关键字,它就会返回“我是全局的”,并且您的范围将得到更改
Rajesh Kumar Bhawsar


41

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

词法部分意味着您可以通过阅读源代码来得出范围。

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

动态范围定义了全局变量,定义后可以在任何地方调用或引用这些全局变量。有时它们被称为全局变量,即使大多数programmin语言中的全局变量具有词法范围。这意味着,可以从读取代码中得出该变量在此上下文中可用的信息。也许必须遵循use或include子句才能找到实例或定义,但是代码/编译器知道该位置的变量。

相比之下,在动态作用域中,您首先搜索本地函数,然后在调用本地函数的函数中搜索,然后在调用该函数的函数中搜索,依此类推,直到调用堆栈。“动态”是指更改,因为每次调用给定函数时,调用堆栈都可能不同,因此该函数可能会根据调用源的不同而使用不同的变量。(请参阅此处

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

有关更多详细信息,请参见此处此处

Delphi / Object Pascal中的一些示例

Delphi具有词法范围。

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()函数对。有关其用途,请参见此处

假设通过注册代码无法预测调用RegisterClass([TmyClass])的时间(通过用户调用按钮的点击方法调用),调用GetClass('TmyClass')的代码将获得结果与否。使用GetClass()调用RegisterClass()不必在单元的词汇范围内;

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


2
实际上,私有是可在定义类的整个单元中访问的。这就是为什么在D2006中引入“严格私有”的原因。
Marco van de Voort,2009年

2
1对于明语(相对于既复杂语言和实施例没有太大的描述)
入时

36

我喜欢@Arak之类的功能全面,与语言无关的答案。由于此问题被标记为JavaScript,因此,我想在一些注释中特别介绍这种语言。

在JavaScript中,作用域的选择是:

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

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

这是演示两种方法的示例。每次您决定如何确定回调的作用域时,都需要执行此操作,因此这适用于Promise,事件处理程序等。

词法

这是您可能Lexical Scoping在JavaScript中使用的回调术语:

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

var downloadManager = {
  initialize: function() {
    $('.downloadLink').on('click', function () {
      this.startDownload();
    }.bind(this)); // Create a function object bound to `this`
  }
//...

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


使用bind不会影响范围。
本·阿斯顿

12

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


12

IBM将其定义为:

程序或段单元中声明适用的部分。在例程中以及在所有嵌套例程中都知道在例程中声明的标识符。如果嵌套例程声明具有相同名称的项目,则外部项目在嵌套例程中不可用。

范例1:

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:

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();

8

词法作用域意味着在一组嵌套函数中,内部函数可以访问其父作用域的变量和其他资源。这意味着子功能在词汇上绑定到其父项的执行上下文。词法作用域有时也称为静态作用域

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';
        }
    }
}

您将注意到的关于词法范围的事情是它向前工作,这意味着可以通过其子级的执行上下文访问名称。但是它对它的父级没有作用,这意味着该变量likes不能被其父级访问。

这也告诉我们,在不同执行上下文中具有相同名称的变量从执行堆栈的顶部到底部获得优先级。最里面的函数(执行堆栈的最上层上下文)中具有与另一个变量相似名称的变量将具有更高的优先级。

请注意,这是从此处获取的


8

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

例:

let str="JavaScript";

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

myFun();

//输出:JavaScript


2
用一个例子,对我来说最短和最好的答案。可以添加ES6的箭头功能来解决问题bind。有了它们,bind不再需要。有关此更改的更多信息,请检查stackoverflow.com/a/34361380/11127383
Daniel Danielecki

4

关于词汇动态作用域的对话中有一个重要的部分丢失了:对范围变量的生存期(或何时可以访问该变量)进行简单说明。

动态作用域仅非常松散地对应于我们传统上考虑的方式的“全局”作用域(我之所以提出两者之间的比较的原因是,它已经被提及了 -我不特别喜欢链接文章的解释); 最好不要在全局变量和动态变量之间进行比较-尽管据链接文章所述,“ ... [它]可以替代全局范围的变量。”

那么,用简单的英语来说,这两种作用域机制之间的重要区别是什么?

在上面的所有答案中,词法作用域的定义非常好:词法范围的变量在定义它的函数的本地级别可用(或可访问)。

但是,由于它不是OP的重点,因此动态作用域还没有引起足够的关注,而它所获得的关注意味着它可能需要更多的关注(这不是对其他答案的批评,而是“哦,这个答案使我们希望还有更多”)。所以,这里还有更多:

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

... [I] n动态范围(或动态范围),如果变量名的范围是某个函数,则其范围是该函数执行的时间段:当函数运行时,变量名存在,并且绑定到其变量,但是在函数返回后,变量名不存在。


3

词法作用域意味着函数在定义它的上下文中而不是在它周围的范围中查找变量。

如果需要更多细节,请查看词汇范围在Lisp中的工作方式。Kyle Cronin在Common Lisp中的Dynamic和Lexical变量中选择的答案比这里的答案要清晰得多。

巧合的是,我只是在Lisp类中了解了这一点,并且它恰好也适用于JavaScript。

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

// 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)
})();                     //)

输出:

5
10
5

3

JavaScript中的词法作用域意味着在函数外部定义的变量可以在变量声明后定义的另一个函数内部访问。但是事实却并非如此。在函数内部定义的变量将无法在该函数外部访问。

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

假设我们有以下代码。

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

现在,当您调用add()->时,将显示3。

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


考虑该代码段是针对动态范围语言的。如果在add()给定的代码片段之后立即调用该函数,则该函数也会打印3。词法作用域并不仅仅意味着函数可以访问局部上下文之外的全局变量。因此,示例代码确实无助于显示词法作用域的含义。在代码中显示词法作用域确实需要一个反例或至少需要对代码的其他可能解释的解释。
C Perkins

2

词法范围是指从执行堆栈中的当前位置可见的标识符(例如,变量,函数等)的词库。

- global execution context
    - foo
    - bar
    - function1 execution context
        - foo2
        - bar2
        - function2 execution context
            - foo3
            - bar3

foo并且bar始终在可用标识符的词典之内,因为它们是全局的。

function1被执行时,它可以访问的词典foo2bar2foo,和bar

function2被执行时,它可以访问的词典foo3bar3foo2bar2foo,和bar

全局和/或外部功能无法访问内部功能标识符的原因是因为尚未执行该功能,因此,没有将其标识符分配给内存。而且,一旦内部上下文执行完毕,就会从执行堆栈中将其删除,这意味着其所有标识符均已被垃圾回收,并且不再可用。

最后,这就是为什么嵌套执行上下文始终可以访问其祖先执行上下文的原因,因此为什么它可以访问更大的标识符词典。

看到:

特别感谢@ robr3rd帮助简化上述定义。


1

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

解释涉及跟踪三件事:

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

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

  3. 给定操作运行的环境 -即状态在操作上的投影。

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

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

这是定义范围界定问题的框架。现在到我们的选择的下一部分。

  • 作为解释器的实现者,您可以通过使环境尽可能接近程序的状态来简化任务。因此,代码行的环境可以简单地由前一行代码的环境定义,并对其施加操作的效果,而不管前一行是否是赋值,函数调用,从函数返回,或诸如while循环之类的控制结构。

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

  • 或者,您可能会想到程序员使用您的语言,并简化了他或她跟踪变量可以采用的值的任务。关于结果的推理和过去执行的总过程涉及太多的路径和太多的复杂性。Lexical Scoping通过将当前环境限制当前块,函数或其他作用域单位及其父级(即,包含当前时钟的块或称为当前函数的函数)中定义的状态部分来帮助实现此目的。

换句话说,对于词汇作用域,任何代码所看到的环境都将绑定到与语言中明确定义的作用域(例如,块或函数)相关联的状态。


0

古老的问题,但这是我的看法。

词法(静态)范围是指源代码中变量的范围。

在像JavaScript这样的语言中,可以传递功能并将其附加和重新附加到其他对象,尽管您的范围可能取决于当时谁在调用该函数,但事实并非如此。以这种方式更改范围将是动态范围,而JavaScript则不会这样做,除非可能使用this对象引用。

为了说明这一点:

var a='apple';

function doit() {
    var a='aardvark';
    return function() {
        alert(a);
    }
}

var test=doit();
test();

在该示例中,变量a是全局定义的,但在doit()函数中带有阴影。如您所见,此函数返回另一个函数,该函数依赖于a于其自身作用域之外变量。

如果运行此命令,则会发现所使用的值是aardvark,而不是apple,尽管它在test()函数不在原始函数的词法范围内。也就是说,所使用的范围是它在源代码中显示的范围,而不是实际使用该功能的范围。

这个事实会产生令人讨厌的后果。例如,您可能决定单独组织函数,然后在需要的时候使用它们(例如在事件处理程序中)会更容易:

var a='apple',b='banana';

function init() {
  var a='aardvark',b='bandicoot';
  document.querySelector('button#a').onclick=function(event) {
    alert(a);
  }
  document.querySelector('button#b').onclick=doB;
}

function doB(event) {
  alert(b);
}

init();
<button id="a">A</button>
<button id="b">B</button>

此代码示例执行每个操作。您可以看到由于词法作用域,button A使用了内部变量,而buttonB没有使用。最终嵌套功能可能会超出您的期望。

顺便说一下,在这两个示例中,您还将注意到即使包含函数功能已经运行,内部词法作用域变量仍然存在。这称为closure,是指嵌套函数对外部变量的访问,即使外部函数已完成也是如此。JavaScript需要足够聪明才能确定是否不再需要这些变量,如果不需要,则可以对其进行垃圾回收。


-1

我通常通过示例学习,这里有一些内容:

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();

-1

该主题与内置bind函数密切相关,并在ECMAScript 6 Arrow Functions中引入。这确实很烦人,因为对于我们要使用的每个新“类”(实际上是函数)方法,我们都必须bind这样做才能访问范围。

JavaScript默认情况下不会将其作用域设置为thison(不会将上下文设置为on this)。默认情况下,您必须明确地说出您想要的上下文

箭头功能自动获取所谓的词法作用域(访问变量的定义在它的包含块)。使用箭头功能时,它会自动绑定this到最初定义箭头功能的位置,并且此箭头功能上下文为其包含块。

在下面最简单的示例中查看其在实践中的工作方式。

在箭头函数之前(默认情况下没有词法范围):

const programming = {
  language: "JavaScript",
  getLanguage: function() {
    return this.language;
  }
}

const globalScope = programming.getLanguage;
console.log(globalScope()); // Output: undefined

const localScope = programming.getLanguage.bind(programming);
console.log(localScope()); // Output: "JavaScript"

带箭头功能(默认为词法作用域):

const programming = {
  language: "JavaScript",
  getLanguage: function() {
    return this.language;
  }
}

const arrowFunction = () => {
    console.log(programming.getLanguage());
}

arrowFunction(); // Output: "JavaScript"
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.