什么是词法作用域的简要介绍?
20 回答
我通过例子来理解它们。:)
首先,词法作用域(也称为静态作用域),采用类似 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
可以访问x
indummy1
或dummy2
,或者任何x
调用fun
其中x
声明的函数中的任何。
dummy1();
将打印 5,
dummy2();
将打印 10。
第一个称为静态,因为它可以在编译时推导出,第二个称为动态,因为外部范围是动态的并且取决于函数的链式调用。
我发现静态范围更容易观察。大多数语言最终都采用了这种方式,即使是 Lisp(两者都可以,对吗?)。动态范围就像将所有变量的引用传递给被调用的函数。
作为为什么编译器不能推断出函数的外部动态范围的一个例子,考虑我们的最后一个例子。如果我们这样写:
if(/* some condition */)
dummy1();
else
dummy2();
调用链取决于运行时条件。如果是真的,那么调用链看起来像:
dummy1 --> fun()
如果条件为假:
dummy2 --> fun()
fun
在这两种情况下的外部范围是调用者加上调用者的调用者等等。
顺便提一下,C 语言不允许嵌套函数,也不允许动态作用域。
让我们尝试最短的定义:
词法作用域定义了如何在嵌套函数中解析变量名:内部函数包含父函数的作用域,即使父函数已经返回。
这就是它的全部!
var scope = "I am global";
function whatismyscope(){
var scope = "I am just a local";
function func() {return scope;}
return func;
}
whatismyscope()()
上面的代码将返回“我只是一个本地人”。它不会返回“我是全球性的”。因为函数 func() 计算最初定义的位置是在函数 whatismyscope 的范围内。
无论它被调用什么(全局范围/甚至在另一个函数中),它都不会受到影响,这就是为什么我是全局的全局范围值不会被打印。
这称为词法作用域,其中“函数使用定义时有效的作用域链执行” - 根据 JavaScript 定义指南。
词法作用域是一个非常非常强大的概念。
希望这可以帮助..:)
词法(AKA 静态)作用域是指仅根据变量在代码文本语料库中的位置来确定变量的作用域。变量总是指它的顶级环境。很好地理解它与动态范围的关系。
范围定义了函数、变量等可用的区域。例如,变量的可用性是在其上下文中定义的,比如定义它们的函数、文件或对象。我们通常称这些局部变量。
词法部分意味着您可以通过阅读源代码得出范围。
词法作用域也称为静态作用域。
动态作用域定义了全局变量,定义后可以从任何地方调用或引用。有时它们被称为全局变量,即使大多数编程语言中的全局变量都是词法范围的。这意味着,它可以通过读取该变量在此上下文中可用的代码得出。也许必须遵循使用或包含子句才能找到实例或定义,但代码/编译器知道该位置的变量。
相比之下,在动态作用域中,首先在本地函数中搜索,然后在调用本地函数的函数中搜索,然后在调用该函数的函数中搜索,依此类推,直至调用堆栈。“动态”指的是变化,因为每次调用给定函数时调用堆栈都可能不同,因此该函数可能会根据调用位置而命中不同的变量。(见这里)
要查看动态范围的有趣示例,请参见此处。
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 中的匿名方法(闭包),因为它们知道调用函数的变量。它不会从那里递归地遵循调用路径,因此不是完全动态的。
我喜欢像@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`
}
//...
据我所知,这些方法在行为上是等效的。
词法作用域意味着在一组嵌套函数中,内部函数可以访问其父作用域的变量和其他资源。
这意味着子函数在词法上绑定到其父函数的执行上下文。
词法作用域有时也称为静态作用域。
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
不能访问该变量。
这也告诉我们,在不同的执行上下文中具有相同名称的变量从执行堆栈的顶部到底部获得优先级。
在最内层函数(执行堆栈的最顶层上下文)中,具有与另一个变量相似的名称的变量将具有更高的优先级。
来源。
JavaScript 中的词法范围意味着在函数外部定义的变量可以在变量声明之后定义的另一个函数内部访问。但事实并非如此。函数内部定义的变量将无法在该函数外部访问。
这个概念在 JavaScript 的闭包中被大量使用。
假设我们有以下代码。
var x = 2;
var add = function() {
var y = 1;
return x + y;
};
现在,当你调用 add() --> 这将打印 3。
因此,add() 函数正在访问x
在方法函数 add 之前定义的全局变量。这是由于 JavaScript 中的词法作用域而被调用的。
用简单的语言来说,词法作用域是在你的作用域之外定义的一个变量,或者上层作用域在你的作用域内自动可用,这意味着你不需要在那里传递它。
例子:
let str="JavaScript";
const myFun = () => {
console.log(str);
}
myFun();
// 输出:JavaScript
词法作用域:在函数外部声明的变量是全局变量,在 JavaScript 程序中随处可见。在函数内声明的变量具有函数作用域,并且仅对该函数内出现的代码可见。
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();
词法范围意味着函数在定义它的上下文中查找变量,而不是在它周围的范围内。
如果您想了解更多细节,请查看Lisp中的词法作用域是如何工作的。Kyle Cronin 在Common Lisp中的 Dynamic and Lexical variables 中选择的答案比这里的答案要清晰得多。
巧合的是,我只在一个 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
围绕词法和动态作用域的对话中缺少一个重要部分:对作用域变量的生命周期或何时可以访问变量的简单解释。
动态范围仅非常松散地对应于我们传统上认为的“全局”范围(我之所以提出两者之间的比较是因为它已经被提到过- 我并不特别喜欢链接文章的解释); 最好不要在全局变量和动态变量之间进行比较——尽管根据链接的文章,“...[它] 作为全局范围变量的替代品很有用。”
那么,简单来说,两种作用域机制之间的重要区别是什么?
词法作用域在上面的答案中定义得很好:词法作用域的变量在定义它的函数的本地级别是可用的 - 或者,可访问的。
但是 - 因为它不是 OP 的重点 - 动态范围界定并没有受到很多关注,它所受到的关注意味着它可能需要更多(这不是对其他答案的批评,而是“哦,这个答案让我们希望有更多”)。所以,这里还有一点:
动态范围意味着更大的程序在函数调用的生命周期内可以访问变量 - 或者在函数执行时。真的,维基百科在解释两者之间的差异方面做得很好。为了不混淆它,这里是描述动态范围的文本:
...[I]n 动态作用域(或动态作用域),如果变量名的作用域是某个函数,那么它的作用域就是函数执行的时间段:函数运行时,变量名存在, 并且绑定到它的变量上,但是函数返回后,变量名不存在了。
词法范围是指从执行堆栈中的当前位置可见的标识符(例如,变量、函数等)的词典。
- global execution context
- foo
- bar
- function1 execution context
- foo2
- bar2
- function2 execution context
- foo3
- bar3
foo
并且bar
总是在可用标识符的词典中,因为它们是全局的。
执行时function1
,它可以访问foo2
、bar2
、foo
和的词典bar
。
执行时function2
,它可以访问foo3
, bar3
, foo2
, bar2
, foo
, 和的词典bar
。
全局和/或外部函数无法访问内部函数标识符的原因是该函数的执行尚未发生,因此其标识符均未分配给内存。更重要的是,一旦内部上下文执行,它就会从执行堆栈中删除,这意味着它的所有标识符都已被垃圾收集并且不再可用。
最后,这就是为什么嵌套的执行上下文总是可以访问它的祖先执行上下文,因此它可以访问更大的标识符词典。
看:
- https://tylermcginnis.com/ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript/
- https://developer.mozilla.org/en-US/docs/Glossary/Identifier
特别感谢@robr3rd帮助简化上述定义。
这是关于这个问题的不同角度,我们可以通过退后一步来看看范围界定在更大的解释框架(运行程序)中的作用。换句话说,假设您正在为一种语言构建一个解释器(或编译器),并负责计算输出,给定一个程序和一些输入。
解释涉及跟踪三件事:
状态 - 即堆和堆栈上的变量和引用的内存位置。
对该状态的操作——即程序中的每一行代码
给定操作运行的环境- 即操作上的状态投影。
解释器从程序的第一行代码开始,计算其环境,在该环境中运行该行并捕获其对程序状态的影响。然后它按照程序的控制流执行下一行代码,并重复该过程直到程序结束。
您为任何操作计算环境的方式是通过编程语言定义的一组正式规则。术语“绑定”经常用于描述程序的整体状态到环境中的值的映射。请注意,我们所说的“整体状态”并不是指全局状态,而是在执行中的任何时候每个可达定义的总和)。
这是定义范围问题的框架。现在到下一部分我们的选择是什么。
- 作为解释器的实现者,您可以通过使环境尽可能接近程序的状态来简化您的任务。因此,一行代码的环境将简单地由前一行代码的环境定义,并应用该操作的效果,而不管前一行是赋值、函数调用、函数返回、或诸如while循环之类的控制结构。
这是动态作用域的要点,其中任何代码运行的环境都绑定到由其执行上下文定义的程序状态。
- 或者,您可以考虑使用您的语言的程序员并简化他或她跟踪变量可以取值的任务。推理过去执行的全部结果涉及太多路径和太多复杂性。词法作用域通过将当前环境限制在当前块、函数或其他范围单元及其父级(即包含当前时钟的块或调用当前函数的函数)中定义的状态部分来帮助做到这一点。
换句话说,对于词法范围,任何代码看到的环境都绑定到与语言中明确定义的范围相关联的状态,例如块或函数。
古老的问题,但这是我的看法。
词法(静态)范围是指源代码中变量的范围。
在像 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 需要足够聪明才能确定这些变量是否不再需要,如果不需要,可以垃圾收集它们。
该主题与内置bind
函数密切相关,并在 ECMAScript 6箭头函数中进行了介绍。这真的很烦人,因为对于我们想要使用的每个新的“类”(实际上是函数)方法,我们必须bind
这样做才能访问范围。
默认情况下,JavaScript 不设置其this
on 函数的范围(它不设置上下文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"
词法作用域意味着函数从定义它们的作用域解析自由变量,而不是从调用它们的作用域。
我通常通过例子来学习,这里有一点:
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();