什么是词法作用域?

845

什么是词法作用域的简要介绍?


107
在第58期播客中,Joel鼓励这样的问题,因为他希望SO成为回答问题的“唯一之地”,即使这些问题已经在其他地方得到了回答。这是一个有效的问题,尽管可以更礼貌地提出它。 - malach
18
可能这个提问者在撰写这个问题时不是(或曾经不是)英语流利的。 - xyhhx
50
问题很客气,他只是说出了自己想要的。你可以自由回答,这里不需要过于敏感的礼貌用语。 - Markus Siebeneicher
38
我认为像这样的问题非常好,因为它们为SO建立了内容。在我看来,如果问题没有付出努力,也无所谓... 因为答案将有很棒的内容,这才是这个留言板上最重要的。 - Jwan622
10
我同意@Jwan622的观点,一个好问题不需要冗长,简洁明了也可以。 - Andrew
显示剩余2条评论
22个回答

798
I understand them through examples. :)
首先,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 可以访问 dummy1 或者 dummy2 中的 x,或者在调用带有声明 x 的任何函数时,可以访问其中的任何 x
dummy1();

将会打印出5,

dummy2();

将会打印出10。

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

我发现静态作用域更容易理解。大多数语言最终都采用了这种方式,即使Lisp也是如此(可以同时进行吧?)。动态作用域就像将所有变量的引用传递给被调用的函数。

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

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

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

dummy1 --> fun()

如果条件为假:
dummy2 --> fun()

在这两种情况下,fun 的外部范围是调用者以及调用者的调用者等等
值得一提的是,C语言不允许嵌套函数或动态作用域。

20
我想指出一个非常易懂的教程,我刚刚找到了它。Arak的例子很好,但对于需要更多示例的人来说可能太简短了(实际上,与其他语言相比..)。看一下这个链接。理解this很重要,因为这个关键字将带领我们了解词法作用域。 - CppLearner
值得注意的是,gcc有一个允许嵌套函数的扩展。 - ember arlynx
24
这是一个好的回答。但问题标记为 JavaScript。因此,我认为这不应该被标记为被接受的答案。特别是在JS中,词法作用域是不同的。 - Boyang
8
非常好的回答。谢谢。@Boyang 我不同意。虽然我不是Lisp的编码人员,但我发现Lisp的例子很有帮助,因为它是动态作用域的一个例子,在JS中你无法得到这个。 - dudewad
6
起初我认为这个例子是有效的C代码,并困惑于C中是否存在动态作用域。也许可以将结尾的免责声明移到代码示例之前? - Yangshun Tay
4
这仍然是一个非常有帮助的答案,但我认为@Boyang是正确的。这个答案提到了“level”,它更接近C语言中的块级作用域。JavaScript默认情况下没有块级作用域,所以在for循环内部是典型的问题。JavaScript的词法作用域仅限于函数级别,除非使用ES6的letconst - icc97

388

让我们尝试最简短的定义:

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

就是这样简单!


58
闭包是指即使父函数已经返回,内部函数仍然可以访问和操作父函数中的变量。因此,“即使父函数已经返回”这句话是闭包的一部分。 - Juanma Menendez
8
感谢您!简单概括词法作用域和闭包:「内部函数可以访问外部函数的变量,并保留对这些变量的访问权限。」 - Dungeon
如果父函数返回了,那岂不意味着嵌套的函数也会返回? - Timothy Pulliam

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

whatismyscope()()
上述代码将返回"I am just a local"。它不会返回"I am a global",因为函数func()计算的是它最初定义的位置,即函数whatismyscope的范围内。无论它被调用时处于什么状态(全局作用域/来自另一个函数甚至),它都不会受到干扰,这就是为什么全局作用域值"I am global"不会被打印出来。这被称为词法作用域,其中"函数使用在其定义时有效的作用域链被执行" -根据JavaScript定义指南。词法作用域是一个非常强大的概念。

7
非常好的解释,我想再补充一点,如果你写一个函数 func() {return this.scope;},那么它将返回"I am global",只需使用关键字this,你的作用域就会改变。 - Rajesh Kumar Bhawsar

54

词法作用域(也叫静态作用域)是指根据变量在代码文本中的位置来确定其作用域。一个变量总是引用其顶层环境。了解它与动态作用域的关系很重要。


49

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

词法部分意味着您可以从源代码中推导出作用域。

词法作用域也被称为静态作用域。

动态作用域定义了可以在定义后从任何地方调用或引用的全局变量。有时它们被称为全局变量,尽管大多数编程语言中的全局变量都是具有词法作用域的。这意味着可以通过阅读代码推导出变量在此上下文中可用。也许需要遵循使用或包含子句以查找实例化或定义,但是代码/编译器知道变量在此位置。

相比之下,动态作用域首先在本地函数中搜索,然后在调用本地函数的函数中搜索,然后在调用该函数的函数中搜索,依此类推,一直到呼叫栈的最上层。 “动态”指的是更改,因为每次调用给定的函数时,调用堆栈可能是不同的,因此该函数可能会命中不同的变量,具体取决于它的调用来源。(请参见这里

有关动态作用域的有趣示例,请参见此处

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

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')的代码将会得到一个结果或者不会得到任何结果。RegisterClass()的调用不必在使用GetClass()的单元的词法范围内。

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


2
实际上,private 可以在定义类的整个单元中访问。这就是为什么 "Strict private" 在 D2006 中被引入的原因。 - Marco van de Voort
4
请将以下英文文本翻译成中文。只返回已翻译的文本内容:+1表示使用通俗易懂的语言(不要使用复杂难懂的语言或缺乏描述的例子)。 - Pops

43

词法作用域指的是在一个嵌套的函数组中,内部函数可以访问其父级作用域中的变量和其他资源

这意味着子函数与其父函数的执行上下文词法绑定。

词法作用域有时也称为静态作用域

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

关于词法作用域(lexical scope)的一点就是它向前工作,这意味着名称可以通过其子执行上下文进行访问。

但它不向后工作到其父级,这意味着变量likes不能被其父级访问。

这也告诉我们,在不同执行上下文中具有相同名称的变量从执行堆栈的顶部向下获得优先权。

具有与另一个变量类似的名称的变量,在最内层函数(执行堆栈的最高上下文)中将具有更高的优先级。

来源


42

JavaScript中的词法作用域意味着在函数外定义的变量可以在该变量声明后定义的另一个函数内访问。但反过来则不成立:在函数内定义的变量将无法在该函数外访问。

这个概念在JavaScript中的闭包中被广泛使用。

假设我们有以下代码。

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

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

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


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

37

我喜欢像 @Arak 这样的专业、不依赖于具体编程语言的回答。不过,既然这个问题被标记为了 JavaScript,那么我想贡献一些非常特定于这种语言的笔记。

在 JavaScript 中,我们的作用域选择有:

  • 原样(无需调整作用域)
  • 词法作用域 var _this = this; function callback(){ console.log(_this); }
  • 绑定 callback.bind(this)

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

下面是一个演示两种方法的示例。每当您决定如何作用域回调函数时,都需要这样做,因此这适用于 promises、事件处理程序等。

词法

下面展示的是 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`
  }
//...

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


2
使用 bind 不会影响作用域。 - Ben Aston

24

简单地说,词法作用域是指在您的作用域之外或上层作用域中定义的变量会自动在您的作用域中可用,这意味着您不需要将其传递到那里。

例子:

let str="JavaScript";

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

myFun();

// 输出:JavaScript


3
对我来说,最简短和最好的答案是一个例子。可以补充说明ES6的箭头函数解决了bind的问题。使用它们时,不再需要bind。有关此更改的更多信息,请访问https://dev59.com/7FsX5IYBdhLWcg3wALXa#34361380。 - Daniel Danielecki

18

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

如果您想了解词法作用域在Lisp中的工作原理,请查看Kyle Cronin在Common Lisp中的动态和词法变量中的选定答案,它比这里的答案清晰得多。

巧合的是,我只是在一节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

2
如果您同意,请点赞:这是最有帮助的答案。 - Clint Eastwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接