JavaScript中的词法作用域/闭包

8

我了解'js'中的函数具有词法作用域(即函数在定义时创建其环境(作用域),而不是在执行时创建)。

function f1() {
    var a = 1;
    f2();
}

function f2() {
    return a;
}
f1(); // a is not defined

当我仅运行 'f()' 时,它会返回内部函数。这是可以理解的,因为 'return' 的作用就是如此!
function f() {
    var b = "barb";
    return function() {
        return b;
    }
}
console.log(b); //ReferenceError: b is not defined

为什么会出现“ReferenceError: b未定义”的错误?

但是上面的内部函数不是可以访问它的空间、f()的空间等吗?既然'b'被返回到全局空间,那么console.log()不应该能够正常工作吗?

然而,当我将'f()'赋值给一个新变量并运行它时:

 var x = f(); 
 x();// "barb"
 console.log(b); //ReferenceError: b is not defined

这将返回“b”,即“barb”,但当您再次运行console.log()时,您将得到“ReferenceError:'b'未定义”;现在'b'不是全局作用域吗?那么为什么'x()'没有像'f()'一样返回内部函数呢?


2
你似乎对作用域的问题不是很在意,而更在意return语句的工作方式。 - Ian
3
你返回的是变量b的值,而不是b本身。 - TommyBs
4个回答

57
您好,我的朋友,您非常困惑。您的第一句话就是错的:
“函数在定义时创建它们的环境(作用域),而不是在执行时创建。”
实际上恰恰相反。定义函数并不会创建作用域。调用函数才会创建作用域。
什么是作用域?
简单地说,作用域是变量的生命周期。您知道,每个变量都有出生、生存和死亡。作用域的开始标志着变量诞生的时间,作用域的结束标志着变量死亡的时间。
起初,只有一个作用域(称为程序作用域或全局作用域)。在此作用域中创建的变量只有在程序结束时才会死亡。它们被称为全局变量。
例如,请考虑以下程序:

const x = 10;       // global variable x

{                   // beginning of a scope
    const x = 20;   // local variable x
    console.log(x); // 20
}                   // end of the scope

console.log(x);     // 10

在这里我们创建了一个名为x的全局变量。然后我们创建了一个块级作用域,在这个块级作用域内,我们创建了一个名为x的局部变量。由于局部变量覆盖全局变量,因此当我们记录x时,我们得到20。回到全局作用域时,我们记录x,我们得到10(局部变量x现在已经失效)。

块级作用域和函数作用域

现在编程中有两种主要的作用域类型-块级作用域和函数作用域。

前面示例中的作用域是块级作用域。它只是一段代码块。因此得名。块级作用域会立即执行。

另一方面,函数作用域是块级作用域的模板。顾名思义,函数作用域属于一个函数。但是更准确地说,它属于一个函数调用。函数作用域在函数被调用之前是不存在的。例如:

const x = 10;

function inc(x) {
    console.log(x + 1);
}

inc(3);         // 4
console.log(x); // 10
inc(7);         // 8

你可以看到每次调用函数都会创建一个新的作用域。这就是为什么你会得到输出结果为4108的原因。
最初,JavaScript只有函数作用域,没有块级作用域。因此,如果你想创建块级作用域,那么你必须创建一个函数并立即执行它:

const x = 10;         // global variable x

(function () {        // beginning of a scope
    const x = 20;     // local variable x
    console.log(x);   // 20
}());                 // end of the scope

console.log(x);       // 10

这种模式被称为立即调用函数表达式(IIFE)。当然,现在我们可以使用constlet创建块级作用域变量。

词法作用域和动态作用域

函数作用域可以再次分为两种类型——词法作用域和动态作用域。你看,在一个函数中有两种类型的变量:

  1. 自由变量
  2. 绑定变量

在作用域内声明的变量绑定到该作用域。未在作用域内声明的变量是自由的。这些自由变量属于其他作用域,但是属于哪个作用域呢?

词法作用域

在词法作用域中,自由变量必须属于父级作用域。例如:

function add(x) {         // template of a new scope, x is bound in this scope
    return function (y) { // template of a new scope, x is free, y is bound
        return x + y;     // x resolves to the parent scope
    };
}

const add10 = add(10);    // create a new scope for x and return a function
console.log(add10(20));   // create a new scope for y and return x + y

像大多数编程语言一样,JavaScript 采用词法作用域。

动态作用域

与词法作用域相反,动态作用域中的自由变量必须属于调用范围(即调用函数的范围)。例如(这也不是 JS - 它没有动态作用域):

function add(y) {   // template of a new scope, y is bound, x is free
    return x + y;   // x resolves to the calling scope
}

function add10(y) { // template of a new scope, bind y
    var x = 10;     // bind x
    return add(y);  // add x and y
}

print(add10(20));   // calling add10 creates a new scope (the calling scope)
                    // the x in add resolves to 10 because the x in add10 is 10

就是这样。很简单,对吧?

问题

你的第一个程序存在问题,那就是JavaScript没有动态作用域,只有词法作用域。看到错误了吗?

function f1() {
    var a = 1;
    f2();
}

function f2() {
    return a;
}

f1(); // a is not defined (obviously - f2 can't access the `a` inside f1)

你的第二个程序非常混乱:

function f() {
    var b = "barb";

    return function() {
        return b;
    }
}

console.log(b); //ReferenceError: b is not defined

这里有一些错误:
  1. 你从未调用 f。因此变量 b 从未被创建。
  2. 即使你调用了 f,变量 b 也只是局部的。
这是你需要做的:

function f() {
    const b = "barb";

    return function() {
        return b;
    }
}

const x = f();

console.log(x());

当你调用x时,它会返回b。但是这并不会使b成为全局变量。要使b成为全局变量,你需要这样做:

function f() {
    const b = "barb";

    return function() {
        return b;
    }
}

const x = f();
const b = x();
console.log(b);

希望这能帮助你理解作用域和函数。

1
我一直在努力理解Eloquent Javascript中的差异,尽管已经阅读了它。非常感谢您提供的详细解释。 - SwankyLegg
2
@Aadit M Shah:您的回答有些矛盾,最好是混淆了。首先,您说:“定义函数不会创建作用域。调用函数才会创建作用域。” 这很好。然后在您的示例中,函数被定义,然后稍后被调用,您为它们都放置了相同的注释“创建作用域”。这与您所说的“定义函数不会创建作用域”相矛盾。然后您引入了术语“调用作用域”,但没有介绍它。它是调用函数的作用域吗?您可能理解这个主题,但我建议您进行一些修改。 - Kevin Le - Khnle
@Khnle-Kevin 当你创建一个函数时,不会创建作用域。当你调用一个函数时,会创建一个词法作用域。当你创建一个函数时,该函数被保存在执行上下文中。执行上下文是在运行程序时创建的内存空间。如果你仍然感到困惑,请观看Udacity上关于作用域和闭包的第一课 https://www.udacity.com/course/object-oriented-javascript--ud015 - Anand
但是,安纳德,这就是你所写的,我逐字引用,请不要编辑你的答案。在第一句话中,"当你创建一个函数时,不会创建作用域"。在第三句话中,"当你创建一个函数时,函数被保留在执行上下文中"。因此,根据你的说法,"当你创建一个函数时,不会创建作用域,函数被保留在执行上下文中"。我发现你所写的非常令人困惑。 - Kevin Le - Khnle
@Khnle-Kevin Anand没有写这个答案。是我写的,我的名字是Aadit M Shah。世界上只有一个Aadit M Shah。此外,在你第一次评论(六个月前)后,我编辑了我的答案。请查看我的答案的修订历史记录。现在它不再自相矛盾。干杯。=) - Aadit M Shah
1
我想补充一下,现在你可以使用 let 来进行块级作用域了(这是一个非常好的补充)。 - HarryH

5
你会得到“ReferenceError:b未定义”的错误,因为你的console.log()调用中没有定义“b”。该函数内部有一个“b”,但外部没有。你所断言的“b被返回到全局空间”是不正确的。
当你调用由“f()”函数返回的函数时,它将返回该闭包变量“b”引用的值的副本。在这种情况下,“b”始终是那个字符串,因此函数返回该字符串。这不会导致符号“b”成为全局变量。

我明白了,如果您省略了“var”,那么控制台日志才能看到“b”,对吗?我唯一的问题是,当我运行'x()'时,为什么它没有像'f()'一样返回内部函数呢? 毕竟,当我将'f()'分配给x变量时,它不是与'f()'做了相同的事情吗?(即输出 return function() { return b; } - Antonio Pavicevac-Ortiz
@antonio,你从未实际运行函数f,你只是声明了它。当你将其赋值给x时,你实际上运行了f,然后将返回的元素(恰好是一个匿名函数)赋值给了x。这就是为什么当你运行x时实际上拥有一个函数的原因。 - Racheet
@Racheet 是的,我假设他只是省略了那一步。如果函数“f”及其返回的函数从未被调用,那么它们将不会执行任何操作 :-) - Pointy

2
    function f1() {
          var a = 1;
          f2();
          }

    function f2() {
       return a;
    }
    f1(); // a is not defined
  1. f2(); does not knows about the a,because you never passed 'a' to it,(That's Scope are created when the functions are defined).Look function f2() would have been able to acess a if it was defined inside f1();[Functions can access the variables in same scope in which they are "DEFINED" and NOT "CALLED"]

    function f() {
       var b = "barb";
       return function(){
                         return b;
                        }
    }
    console.log(b); 
    
  2. First of all You Need to Call f(); after executing f(); it would return another function which needs to be executed. i.e

    var a=f();
    a();
    

    it would result into "barb" ,In this case you are returning a function not the var b;

    function f() {
       var b = "barb";
       return b;
                 };
    
    console.log(f());
    

    This would print barb on screen


哇,谢谢你的提示:“它们被定义”而不是“被调用”,救了我一命。我正在学习闭包,读完他的问题后感到非常困惑,直到看到了你的那句话。 - user308553
你在第二点上是正确的;但是请注意,你可以简单地执行 console.log(f()()) 来打印出 "barb"(在这种情况下,OP 的 "f" 函数显然是一个不好的设计模式 -- 只是指出了一种更简洁的语法,如果有人试图从返回函数的函数中获取值)。 - squidbe

2

但是上面的内部函数有没有访问它的空间,f()的空间等。

有的。它访问了变量b并从函数中返回其值

既然'b'被返回到全局空间中

不是的。从函数返回一个值并不是“在调用者范围内使变量可用”。调用函数(使用f())是一个表达式,其结果是函数返回的值(在您的情况下是未命名的函数对象)。该值可以分配给某个地方(例如x),可以访问其属性或可以丢弃它。

然而,变量b仍然保持私有,存在它被声明的范围内。它没有在调用console.log的范围内定义,这就是为什么会出现错误。

你想要的似乎是

var x = f();
var b = x(); // declare new variable b here, assign the returned value
console.log( b ); // logs "barb"

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