JavaScript中创建全局变量和在构造函数中创建变量的区别

3
如果我声明一个全局变量x:
var x = "I am window.x";

x将成为window对象的公共属性。如果我调用一个全局函数(不使用“call”,“apply”或先将其附加到另一个对象上),则window对象将作为上下文(“this”关键字)传递进来。这就像是将x属性放在当前上下文上,而当前上下文恰好是window。
然而,如果我在函数内以相同的方式声明变量,然后将该函数用作构造函数,则刚刚构造的对象(当前上下文)的属性x将不是公共属性。我很高兴(我知道我可以做this.x = ...),但这似乎有点矛盾。
我是否误解了某些东西(关于它是矛盾/具有不同行为的问题)?是否有人能够解释一下发生了什么,还是我必须接受这种情况?
希望我的问题很清楚。

“this” 不是上下文。如果您使用未经合格的引用来调用函数,则其this关键字将设置为全局对象,而不是“传入”。ECMA-262 将执行上下文定义为范围、变量、参数、this 和进入函数执行上下文时设置的其他所有内容。它比仅仅“this”(可以通过调用设置为任何内容)要多得多。 - RobG
3个回答

5
似乎你已经理解得很好了(我对你下面的术语有一点小问题)。构造函数内的局部变量只是构造函数内的局部变量,它们根本不是由构造函数初始化的实例的一部分。
这都是 JavaScript 中“作用域”的结果。当你调用一个函数时,会为该函数调用创建一个“执行上下文”(EC)。EC 有一个称为“变量上下文”的东西,其中有一个“绑定对象”(让我们称之为“变量对象”,好吗?)。变量对象保存在函数内定义的所有 var 和函数参数等内容。这个变量对象是非常真实和非常重要的闭包工作方式,但你不能直接访问它。构造函数中的 x 是为调用构造函数创建的变量对象的属性。
所有作用域都有一个变量对象;神奇的是,全局作用域的变量对象是全局对象,在浏览器中是window。(更准确地说,window是变量对象上的一个属性,指向变量对象本身,因此可以直接引用它。函数调用中的变量对象没有任何等效的属性。) 因此,在全局作用域中定义的xwindow的属性。
我承诺的术语细节:你说过:

如果调用全局函数,则会将window对象作为上下文(“this”关键字)传递。

这基本上是正确的。例如,如果您像这样调用全局函数:
myGlobalFunction();

如果你以这种方式调用全局函数,this将是全局对象(window)。但是,还有许多其他可能调用该全局函数的方式,此时它不是全局对象。例如,如果你将该“全局”函数分配给对象上的属性,然后通过该属性调用函数,则调用中的this将是该属性所属的对象:
var obj = {};
obj.foo = myGlobalFunction;
obj.foo();    // `this` is `obj` within the call, not `window`
obj['foo'](); // Exactly the same as above, just different notation

或者你可以使用函数对象的 callapply 特性来显式地设置 this

var obj = {};
myGlobalFunction.call(obj, 1, 2, 3);    // Again, `this` will be `obj`
myGlobalFunction.apply(obj, [1, 2, 3]); // Same (`call` and `apply` just vary
                                        // in terms of how you pass arguments

更多探索(声明:这些是我的博客链接,但没有广告或其他东西,似乎不太可能添加):


更新:你在下面说:

我只想确认我的理解:在任何范围(全局或函数)中,总会有两个对象:一个“this”对象(那叫什么?)和一个“变量对象”。在全局范围内,这两个对象是相同的。在函数的范围内,它们是不同的,并且“变量对象”是不可访问的。这是正确的吗?

你走在正确的轨道上了,是的,总是有那两个东西存在(通常还有更多;请参见下文)。但是,“作用域”和 this 没有任何关系。如果你从其他语言转到 JavaScript,这很令人惊讶,但这是真的。在 JavaScript 中,this(有时称为“上下文”,尽管这可能会误导)完全由调用函数的方式定义,而不是定义函数的位置。您在调用函数时以几种方式设置this(请参见答案和上面的链接)。从 this 的角度来看,在全局范围内定义的函数和在其他函数内部定义的函数之间没有任何区别。没有。
但是,在 JavaScript 代码中(无论在哪里定义),总会有 this 和一个变量对象。实际上,经常有多个变量对象,排成一条链,这被称为 作用域链。当您尝试检索自由变量的值(未限定的符号,例如 x 而不是 obj.x)时,解释器在最顶层的变量对象中查找具有该名称的属性。如果找不到,则转到链中的下一个链接(下一个外部作用域)并在其变量对象上查找。如果它没有,则查看链中的下一个链接,以此类推。那么,您知道链中的最后一个链接是什么吗?没错!全局对象(在浏览器上为 window)。
考虑以下代码(假设我们从全局作用域开始; live copy):
var alpha = "I'm window.alpha";
var beta  = "I'm window.beta";

// These, of course, reference the globals above
display("[global] alpha = " + alpha);
display("[global] beta  = " + beta);

function foo(gamma) {
    var alpha = "I'm alpha in the variable object for the call to `foo`";

    newSection();
    // References `alpha` on the variable object for this call to `foo`
    display("[foo] alpha = " + alpha);
    // References `beta` on `window` (the containing variable object)
    display("[foo] beta  = " + beta);
    // References `gamma` on the variable object for this call to `foo`
    display("[foo] gamma = " + gamma);

    setTimeout(callback, 200);

    function callback() {
        var alpha = "I'm alpha in the variable object for the call to `callback`";

        newSection();
        // References `alpha` on the variable obj for this call to `callback`
        display("[callback] alpha = " + alpha);
        // References `beta` on `window` (the outermost variable object)
        display("[callback] beta  = " + beta);
        // References `gamma` on the containing variable object (the call to `foo` that created `callback`) 
        display("[callback] gamma = " + gamma);
    }
}

foo("I'm gamma1, passed as an argument to foo");
foo("I'm gamma2, passed as an argument to foo");

function display(msg) {
    var p = document.createElement('p');
    p.innerHTML = msg;
    document.body.appendChild(p);
}
function newSection() {
    document.body.appendChild(document.createElement('hr'));
}

输出结果如下:
[global] alpha = 我是 window.alpha [global] beta = 我是 window.beta -------------------------------------------------------------------------------- [foo] alpha = 我是调用 `foo` 时的变量对象中的 alpha [foo] beta = 我是 window.beta [foo] gamma = 我是作为参数传递给 foo 的 gamma1 -------------------------------------------------------------------------------- [foo] alpha = 我是调用 `foo` 时的变量对象中的 alpha [foo] beta = 我是 window.beta [foo] gamma = 我是作为参数传递给 foo 的 gamma2 -------------------------------------------------------------------------------- [callback] alpha = 我是调用 `callback` 时的变量对象中的 alpha [callback] beta = 我是 window.beta [callback] gamma = 我是作为参数传递给 foo 的 gamma1 -------------------------------------------------------------------------------- [callback] alpha = 我是调用 `callback` 时的变量对象中的 alpha [callback] beta = 我是 window.beta [callback] gamma = 我是作为参数传递给 foo 的 gamma2
你可以看到作用域链的工作原理。在调用 `callback` 时,作用域链从上到下依次为:
  • 回调函数callback的变量对象
  • 创建callback的调用foo的变量对象
  • 全局对象

注意,调用foo的变量对象在foo函数结束后仍然存在(foosetTimeout调用callback之前返回)。这就是闭包的工作原理。当一个函数被创建时(注意,每次调用foo都会创建一个新的callback函数对象),它会获得对那个时刻顶部作用域链中的变量对象的持久引用(整个变量对象,不仅仅是我们看到它引用的那些部分)。因此,在等待两个setTimeout调用发生的短暂时刻内,我们在内存中有两个调用foo的变量对象。还要注意,函数参数的行为与var完全相同。以下是上述内容的运行时详细说明:

  1. 解释器创建全局作用域。
  2. 它创建全局对象并使用其默认属性集(windowDateString和所有其他您习惯拥有的“全局”符号)来填充它。
  3. 它为全局范围内的所有var语句在全局对象上创建属性;最初它们的值为undefined。所以在我们的例子中,是alphabeta
  4. 它为全局范围内所有函数声明在全局对象上创建属性;最初它们的值为undefined。所以在我们的例子中,是foo和我实用程序函数displaynewSection
  5. 它按顺序处理全局范围内的每个函数声明
    • 创建函数对象
    • 将其分配给当前变量对象的引用(在本例中为全局对象)
    • 将函数对象分配给其在变量对象上的属性(再次,在本例中为全局对象)
  6. 解释器开始执行逐步代码,从顶部开始。
  7. 它到达的第一行是var alpha = "I'm window.alpha";。当然,它已经完成了这个var方面的工作,所以它将其处理为直接赋值。
  8. var beta = ...也是如此。
  9. 它两次调用display(省略细节)。
  10. foo函数声明已经被处理过了,并且根本不是逐步代码执行的一部分,因此解释器到达的下一行是foo("I'm gamma1, passed as an argument to foo");
  11. 它为调用foo创建执行上下文。
  12. 它为此执行上下文创建变量对象,为方便起见,我将其称为foo#varobj1
  13. 它将foo#varobj1分配一个副本,该副本是foo对创建foo的变量对象的引用(在本例中为全局对象);这是它与“作用域链”的链接。
  14. 解释器为foo#varobj1中所有命名的函数参数、varfoo内部的函数声明创建属性。所以在我们的例子中,那是gamma(参数)、alphavar)和callback(声明的函数)。最初它们的值为undefined。(这里还创建了一些其他默认属性,我不会详细介绍。)
  15. 它将函数参数的属性分配给传递给函数的值。
  16. 它按顺序处理foo中的每个函数声明(从头到尾)。在我们的例子中,那是callback
    • 创建函数对象
    • 将该函数对象分配给当前变量对象的引用(foo#varobj1
    • 将函数对象分配给其在foo#varobj1上的属性
  17. 解释器开始逐步执行foo代码
  18. 哎呀,这太有趣了,不是吗?

    关于上述内容的最后一点:请注意 JavaScript 作用域是如何完全由源代码中函数的嵌套确定的;这被称为“词法作用域”。例如,调用堆栈对变量解析并不重要(除了在函数创建时获取对其所在作用域中变量对象的引用之外),只有源代码中的嵌套。考虑以下示例(实时副本):

    var alpha = "I'm window.alpha";
    
    function foo() {
        var alpha = "I'm alpha in the variable object for the call to `foo`";
    
        bar();
    }
    
    function bar() {
    
        display("alpha = " + alpha);
    }
    
    foo();
    

    对于alpha,最终输出的是什么?没错!"I'm window.alpha"。我们在foo中定义的alphabar没有任何影响,尽管我们是从foo中调用了bar。让我们快速地回顾一下:

    1. 设置全局执行上下文等等。
    2. var和声明的函数创建属性。
    3. alpha赋值。
    4. 创建foo函数对象,给它一个对当前变量对象(即全局对象)的引用,并将其放在foo属性上。
    5. 创建bar函数对象,给它一个对当前变量对象(即全局对象)的引用,并将其放在bar属性上。
    6. 通过创建执行上下文和变量对象来调用foo。变量对象foo#varobj1得到了foo对其父变量对象的引用的副本,该父变量对象当然是全局对象。
    7. 开始逐步执行foo的代码。
    8. 查找自由变量bar,在全局对象中找到。
    9. 为调用bar及其关联的变量对象bar#varobj1创建执行上下文。将bar#varobj1分配一个对其父变量对象的引用的副本,该父变量对象当然是全局对象。
    10. 开始逐步执行bar的代码。
    11. 查找自由变量alpha
      • 首先在bar#varobj1上查找,但没有该名称的属性。
      • 因此它查找下一个链接,即从bar获取的链接,即全局对象。所以它找到了全局的alpha
    请注意,foo#varobj1bar的变量对象没有任何关联。这是好事,因为如果作用域由函数被调用的方式和位置定义,我们都会疯掉的。 :-) 一旦你理解它与函数创建相关联,而函数创建受源代码的嵌套所决定,就会更容易理解。

    这就是为什么bar的作用域完全由它在源代码中的位置决定,而不是运行时如何调用。

    一开始你可能会想到this和变量解析之间的关系,这并不奇怪,因为在JavaScript中全局对象(window)有两个无关的作用:1. 如果函数没有以设置不同的值的方式被调用(在全局范围内),它是默认的this值,2. 它是全局变量对象。这些是解释器使用全局对象的无关方面,这可能会令人困惑,因为当this === window时,似乎变量解析与this有关,但实际上并不是这样。一旦你开始使用其他东西来代替thisthis和变量解析就完全脱钩了。


我只是想确认我的理解:在任何范围(全局或函数)中都有两个对象:一个“this”对象(叫做什么?)和一个“变量对象”。在全局范围内,这两个对象是相同的。在函数作用域中,它们是不同的,“变量对象”是不能访问的。是这样吗? - zod
@zod:我已经更新了答案以回应那个评论。希望这能有所帮助。 - T.J. Crowder
@zod:我是个疯子,我刚刚添加了一些详细的代码演示来真正展示它。必须写博客记录下来。 - T.J. Crowder

0

你对属性和构造函数的理解很好;你所缺少的概念是“作用域”和“闭包”。这就是var发挥作用的地方。

尝试阅读Robert Nyman的解释


0

您可以在这个代码片段中找到一些示例:

var x = 42;

function alertXs() {
    this.x = 'not 42'; // this = window
    var x = '42 not'; // local x

    alert('window.x = ' + window.x); // 'not 42'
    alert('this.x = ' + this.x);     // 'not 42'
    alert('x = ' + x);               // '42 not'

}

alertXs();

http://jsfiddle.net/Christophe/Pgk73/

有时候,创建小的代码片段可以帮助理解...
但是你对本地和公共变量非常了解,因为你解释得非常好...

活链接是答案的一个很好的附属,但始终要在答案中发布相关代码。有两个原因。1.人们不应该跟随链接来查看你在说什么。2.StackOverflow旨在为将来遇到类似问题的其他人提供资源,而不仅仅是现在的提问者。外部链接可能会被移动、修改、删除等。通过确保相关代码在答案中,我们确保答案在合理的时间内仍然有用。 - T.J. Crowder

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