什么是词法作用域?

845

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


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

15

词法作用域:在函数外部声明的变量是全局变量,可以在 JavaScript 程序的任何地方看到。在函数内部声明的变量具有函数范围,只能被出现在该函数内部的代码看到。


13

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

7

希望这对你有所帮助,以下是我对稍微抽象一些的定义的尝试:

词法作用域: 根据源代码中某个元素(如函数或变量)的位置确定其对程序中其他元素的访问或范围。

顺便提一下,我的逻辑是基于以下定义构建的:

词法:与语言的单词或词汇有关(特别是单词与其语法或结构分离的情况){在我们的例子中-一种编程语言}。

作用域(名词):操作范围{在我们的例子中,范围是指可以访问的内容}。

请注意,ALGOL 60规范中最初的1960年定义“词法作用域”的原始定义比我上面的尝试更为简洁:

词法作用域:名称与实体绑定时适用于源代码部分。来源


6

词法作用域指的是当前执行栈中可见的标识符(例如,变量、函数等)的词汇表。

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

foobar是全局可用的标识符,因此它们总是存在于可用词汇表中。

当执行function1时,它可以访问foo2bar2foobar的词汇表。

当执行function2时,它可以访问foo3bar3foo2bar2foobar的词汇表。

全局和/或外部函数无法访问内部函数的标识符,因为该函数尚未执行,因此其任何标识符都尚未分配到内存。而且,一旦该内部上下文执行完毕,就会从执行堆栈中删除它,这意味着其中所有的标识符已被清除并不再可用。

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

参见:

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


这就解释了为什么它被称为“词法作用域”,并形成了相同的良好心理模型,简直是太美妙了。 - shiva addanki

4

虽然这是一个古老的问题,但以下是我的看法。

词法(静态)作用域是指一个变量在源代码中的范围。

在像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>

这个代码示例包含了两种情况。我们可以看到由于词法作用域,按钮A使用内部变量,而按钮B则不使用。您可能会发现自己需要嵌套更多的函数。

另外,无论在哪个示例中,都会注意到内部词法范围的变量仍然存在,即使包含函数已经执行完毕。这被称为闭包,指的是嵌套函数对外部变量的访问,即使外部函数已经完成。JavaScript需要足够聪明地确定是否不再需要这些变量,如果不需要,则可以进行垃圾回收。


4
在关于词法作用域和动态作用域的讨论中,有一个重要的部分被忽略了:即作用域变量的生命周期——或者说何时可以访问该变量。
动态作用域与我们传统思考方式中的“全局”作用域只存在非常松散的相似之处(我提到这两者之间的比较是因为它已经被提到过mentioned,但我并不特别喜欢linked文章的解释)。最好不要将全局和动态作用域进行比较,虽然根据链接文章,“...[它]可作为全局作用域变量的替代品”。
那么,用简单的语言来说,这两种作用域机制之间的重要区别是什么?
词法作用域已经在上面的答案中得到了很好的定义:在定义该变量的函数的本地级别上,词法作用域变量是可用的——或者说是可访问的。
然而,由于它不是OP的重点,动态作用域并没有得到太多关注,而且它所受到的关注意味着它可能需要更多(这并不是对其他答案的批评,而是“哦,那个答案让我希望有更多的东西”)。因此,这里有更多的内容:
动态作用域意味着变量在函数调用期间或函数执行期间对整个程序可访问。实际上,维基百科在两者之间的差异解释方面做得很好。为了不使其模糊,这是描述动态作用域的文本:
引用: “...在动态作用域中(或动态范围),如果变量名称的作用域是某个函数,则其作用域是函数正在执行的时间段:当函数正在运行时,变量名称存在,并绑定到其变量,但在函数返回后,变量名称不存在。”

2

作用域是变量/绑定可访问的上下文环境。词法作用域意味着局限于封闭的词法代码块中,而不是全局作用域。


2
这个问题可以从另一个角度来看待,即从解释(运行程序)的更大框架中考虑作用域的作用。换句话说,想象一下,如果你正在为一种语言构建解释器(或编译器),并且负责计算输出结果,则需要跟踪以下三个方面:
1.状态-即堆和栈上的变量和引用内存位置。 2.对该状态的操作-即程序中的每行代码。 3.给定操作运行的环境-即在操作上投影状态。
解释器从程序的第一行代码开始,计算其环境,在该环境中运行该行,并捕获其对程序状态的影响。然后,它按照程序的控制流程执行下一行代码,并重复该过程直到程序结束。
为任何操作计算环境的方式是通过编程语言定义的一组正式规则。术语“绑定”通常用于描述将程序的整体状态映射到环境中的值。请注意,“整体状态”并不意味着全局状态,而是在执行的任何时刻可达的每个定义的总和。
这就是作用域问题所定义的框架。接下来是我们的选择。
作为解释器的实现者,您可以通过使环境尽可能接近程序的状态来简化任务。因此,代码行的环境将仅由先前代码行的环境定义,并将其操作的效果应用于它,而不管先前的行是赋值、函数调用、从函数返回还是控制结构(例如while循环)。
这就是动态作用域的要点,其中任何代码运行的环境都绑定到程序状态所定义的执行上下文。
或者,您可以考虑使用您的语言的程序员,并简化他或她跟踪变量可能采取的值的任务。在推理关于过去执行的总体结果时涉及太多路径和复杂性。词法作用域通过将当前环境限制为当前块、函数或其他作用域单元中定义的状态部分以及其父级(即包含当前块的块或调用当前函数的函数)来帮助实现这一点。
换句话说,对于词法作用域,任何代码看到的环境都绑定到显式定义的语言范围内的状态,例如块或函数。

1

为了理解词法环境,让我们先了解作用域的概念:

作用域:

每当你在JavaScript中创建一个函数时,你就创建了一个作用域,这意味着该方法内部的变量将无法从外部访问。在JavaScript中可以有多个作用域。首先是全局作用域。然后假设你定义了一个名为foo的函数,那么你就创建了由foo创建的作用域,现在你在foo内部定义了另一个名为bar的方法,那么你就在foo的作用域内创建了另一个由bar创建的作用域。

function foo(){
let name="foo"
function bar(){
console.log(name)
}
return bar
}

现在,当我们在bar内部引用变量name时,JavaScript将无法在bar中找到任何内容,因此JavaScript将查找其外部作用域foo并找到该变量,然后使用它。这些作用域的链条称为作用域链。

因此,词法环境基本上为您提供了这个作用域链和一组规则,以便使用它来解析标识符。总体而言,这被称为函数的词法环境,并在运行时用于解析标识符。

请记住,JavaScript在编译阶段定义了这个作用域,因此被称为静态作用域。


0

词法作用域意味着函数解析自由变量的范围是它们被定义的作用域,而不是它们被调用的作用域。


释放某些东西与词法作用域无关,而且只在非GC语言中才是相关的考虑。 - Samantha Atkins

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