创建全局变量与使用JavaScript的构造函数中的变量之间的区别

zod*_*zod 3 javascript

如果我将全局变量x声明为:

var x = "I am window.x";
Run Code Online (Sandbox Code Playgroud)

x将是window对象的公共属性。如果我调用全局函数(不先使用“ call”,“ apply”或将其附加到另一个对象),则将窗口对象作为上下文传递(“ this”关键字)。就像是将x属性放在当前上下文中一样,该上下文恰好是窗口。

但是,如果我在函数内部以相同的方式声明变量,然后将该函数用作构造函数,则属性x将不是我刚刚构造的对象(当前上下文)的公共属性。我很高兴(我知道我可以做到这一点。x=…),但这似乎有点矛盾。

我是否误解了一些东西(关于它是一种矛盾/不同的行为)?谁能解释发生了什么,还是我必须接受的事情?

希望我的问题清楚。

T.J*_*der 5

看来您已经了解得很好(我在下面用您的术语选择了一个小巧的选择)。构造函数内的局部变量就是:构造函数内的局部变量。它们根本不是构造函数初始化的实例的一部分。

这都是“范围”在JavaScript中的工作方式的结果。调用函数时,将为该函数调用创建执行上下文(EC)。EC有一个叫做变量上下文的东西,它有一个绑定对象(我们就称它为“变量对象”,是吗?)。变量对象包含所有vars和函数参数以及函数内定义的其他内容。这个可变对象是很真实的东西,对于闭包的工作方式非常重要,但是您不能直接访问它。您x在构造函数中是为调用构造函数而创建的变量对象的属性

所有作用域都有一个变量对象。魔术是全局范围的变量对象是window浏览器上的全局对象。(更准确地说,window是变量对象上的一个属性,它引用回该变量对象,因此您可以直接引用它。函数调用中的变量对象没有任何等效属性。)因此,x您在全局范围内定义的是的属性window

我承诺的那种挑剔的术语:您已经说过:

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

多数情况是这样。例如,如果您像这样调用全局函数:

myGlobalFunction();
Run Code Online (Sandbox Code Playgroud)

...然后是,在调用过程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
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用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
Run Code Online (Sandbox Code Playgroud)

需要探索的内容更多(公开:这些是指向我的博客的链接,但其中没有广告或任何内容,似乎不太可能添加它们):


更新:您在下面说过:

我只是想检查一下我的理解:在任何范围(全局或函数)中,始终有2个对象:“此”对象(称为“什么”?)和“变量对象”。在全局范围内,这两个对象是相同的。在函数范围内,它们是不同的,并且“变量对象”不可访问。那是对的吗?

您处在正确的轨道上,是的,总是有这两个问题在发生(通常是更多;请参阅下文)。但是“范围”与this彼此无关。如果您要使用其他语言的JavaScript,这是令人惊讶的,但这是事实。this在JavaScript中(有时可能会误导用户,有时称为“上下文”)完全由函数的调用方式定义,而不是由函数定义的地方定义。您可以this通过几种方式中的任何一种进行设置(请参见上面的答案和链接)。从一个this角度看,定义全局范围的函数与定义在另一个函数内的函数之间没有任何区别。零。齐尔奇

但是,是的,在JavaScript代码(无论在何处定义)中,始终存在this,可以是任何东西和一个变量对象。实际上,通常有多个可变对象按链排列。这称为范围链。当您尝试检索自由变量的值(不合格的符号,例如x而不是obj.x)时,解释器会在最上方的变量对象中查找具有该名称的属性。如果找不到,则转到链中的下一个链接(下一个外部作用域),然后查看变量对象。如果没有,则查看链中的下一个链接,依此类推,以此类推。而且您知道链中的最终链接是什么,对吗?对!全局对象(window在浏览器上为)。

考虑以下代码(假设我们从全局范围开始;实时复制):

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'));
}
Run Code Online (Sandbox Code Playgroud)

输出是这样的:

[global] alpha =我是window.alpha
[global] beta =我是window.beta
-------------------------------------------------- ------------------------------
[foo] alpha =我是变量对象中的alpha,用于调用foo
[foo] beta =我是window.beta
[foo] gamma =我是gamma1,作为参数传递给foo
-------------------------------------------------- ------------------------------
[foo] alpha =我是变量对象中的alpha,用于调用foo
[foo] beta =我是window.beta
[foo] gamma =我是gamma2,作为参数传递给foo
-------------------------------------------------- ------------------------------
[callback] alpha =我是变量对象中的alpha,用于调用`callback`
[回调] beta =我是window.beta
[回调] gamma =我是gamma1,作为参数传递给foo
-------------------------------------------------- ------------------------------
[callback] alpha =我是变量对象中的alpha,用于调用`callback`
[回调] beta =我是window.beta
[回调] gamma =我是gamma2,作为参数传递给foo

您可以在那里看到作用域链。通话至时callback,链为(从上到下):

  • 该调用的变量对象 callback
  • 对于调用变量对象foo创建callback
  • 全局对象

请注意,用于调用的变量对象如何在函数结束之后(在被调用之前返回)中foo 生存。这就是闭包的工作方式。创建函数时(请注意,每次调用都会创建一个新的函数对象),从那时起,它会在作用域链顶部获得对变量对象的持久引用(整个过程,而不仅仅是我们所得到的位)参见参考)。因此,在等待两个调用发生的短暂时间里,我们有两个变量对象用于内存中的调用。还要注意,函数的参数的行为与s 完全相同。这是上面细分的运行时:foofoocallbacksetTimeout callbackfoosetTimeoutfoovar

  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为内部的所有命名函数参数,vars和函数声明创建属性foo。因此,在我们的例子中,这就是gamma(参数),alphavar)和callback(声明的函数)。最初,它们具有价值undefined。(这里将创建一些其他默认属性,我将不再赘述。)
  15. 它为函数参数的属性分配传递给函数的值。
  16. 它处理的每个功能声明foo(按顺序开始到结束)。在我们的情况下,这是callback
    • 创建功能对象
    • 为该功能对象分配对当前变量对象(foo#varobj1)的引用
    • 将功能对象分配给其属性 foo#varobj1
  17. 解释器开始逐步执​​行foo代码
  18. 它处理该var alpha = ...行的赋值,并给出foo#varobj1.alpha其值。
  19. 它查找free变量newSection并调用该函数(详细信息省略,稍后我们将详细介绍)。
  20. 它查找自由变量alpha
    • 首先看foo#varobj1。由于foo#varobj1具有该名称的属性,因此使用该属性的值。
  21. 它查找display并调用它(详细信息省略)。
  22. 它查找自由变量beta
    • 首先,它看起来像foo#varobj1,但foo#varobj1没有该名称的属性
    • 它通过查询foo#varobj1对下一个链接的引用来查找作用域链中的下一个链接
    • 它查找下一个链接(在这种情况下恰好是全局对象),通过该名称找到一个属性,并使用其值
  23. 它呼吁 display
  24. 它查找gamma并呼叫display。这与上面的完全相同alpha
  25. 它查找自由变量callback,找到它foo#varobj1
  26. 它查找自由变量setTimeout,在全局对象上找到它
  27. 它调用setTimeout,传入参数(省略详细信息)
  28. 返回foo。此时,如果没有任何引用foo#varobj1,则可以回收该对象。但是,由于浏览器的计时器内容具有对callback功能对象的引用,而callback功能对象具有对的引用foo#varobj1foo#varobj1一直存在直到/除非不再有任何引用。这是关闭的关键。
  29. 对的第二个调用进行清洗/漂洗/重复foo,这将创建foo#varobj2和的另一个副本callback,为该第二callback个引用分配一个引用foo#varobj2,并最终将该第二个传递callbacksetTimeout并返回。
  30. 解释器用尽了逐步执行的代码,并进入其事件循环,等待某些事情发生
  31. 经过约200毫秒
  32. 浏览器的计时器内容告诉解释器它需要调用callback我们在其中创建的第一个函数foo
  33. 解释器callback#varobj1为调用创建执行上下文和关联的变量对象();它分配callback#varobj1存储在callback函数对象上的变量对象引用(当然是foo#varobj1)的副本,以便建立作用域链。
  34. 它创建了一个属性,alphacallback#varobj1
  35. 它开始逐步执​​行callback的代码
  36. 你知道接下来会发生什么。它查找各种符号并调用各种函数:
    • 查找newSection,找不到它callback#varobj1,因此查看下一个链接foo#varobj1。找不到它,而是查看下一个链接(即全局对象)并找到它。
    • 查找alpha,它在最上面的变量对象上找到,callback#varobj1
    • 查找beta,它没有找到,直到它下到全局对象
    • 查找gamma,它仅在作用域链上找到一个链接foo#varobj1
  37. 口译员从呼叫返回 callback
  38. 几乎可以肯定的是,事件队列中有来自浏览器的消息正在等待它,告诉它调用第二个callback函数,该函数是我们在对的第二次调用中创建的foo
  39. 因此,它又做了一遍。这次,要调用的变量对象callback获得了引用,foo#varobj2因为这就是存储在此特定callback函数对象上的内容。因此(除其他事项外)它看到gamma我们传递给第二个调用而不是第一个调用的参数。
  40. 由于浏览器现在已经发布了其引用的两个callback函数对象,他们和他们的对象是指(包括foo#varobj1foo#varobj2,和任何它们的属性指出,像gamma的字符串)都符合垃圾回收。

哇,那很有趣,是吗?

关于上述内容的最后一点是:注意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();
Run Code Online (Sandbox Code Playgroud)

最终得到什么输出alpha?对!"I'm window.alpha"。在alpha我们的定义foo有任何作用的bar,即使我们称之为barfoo。让我们快速浏览一下:

  1. 设置全局执行上下文等。
  2. vars和声明的函数创建属性。
  3. 分配alpha其值。
  4. 创建foo函数对象,为其提供对当前变量对象(即全局对象)的引用,并将其放在foo属性上。
  5. 创建bar函数对象,为其提供对当前变量对象(即全局对象)的引用,并将其放在bar属性上。
  6. 调用foo通过创建执行上下文和可变对象。变量对象foo#varobj1会获得foo对其父变量对象(当然是全局对象)的引用的副本。
  7. 开始逐步执​​行foo的代码。
  8. 查找free变量bar,它在全局对象上找到。
  9. 创建对调用bar及其关联变量对象的执行上下文bar#varobj1。将的引用bar#varobj1副本分配bar给其父变量对象,该对象当然是全局对象。
  10. 开始逐步执​​行bar的代码。
  11. 查找自由变量alpha
    • 首先,它看起来像bar#varobj1,但是那里没有该名称的属性
    • 因此,它将查看下一个链接,这是它从那里获得的链接bar,这是全局对象。因此它找到了全球alpha

请注意,foo#varobj1根本没有链接到bar的变量对象。那很好,因为如果范围是由调用函数的方式和位置定义的,我们都会发疯。:-)一旦你明白它链接到功能的创建,这是由源代码的嵌套决定的,它得到了很多更容易理解。

这就是为什么范围的bar内容完全由bar源代码中的位置而不是在运行时如何调用决定的。

刚开始您想知道this变量分辨率与变量之间的关系就不足为奇了,因为全局对象(window)在JavaScript中有两个不相关的目的:1. this如果未以设置其他函数的方式调用函数,则它是默认值(并在全局范围内),以及2.它是全局变量对象。这些是解释器与全局对象使用的无关的方面,这可能会造成混淆,因为当this===时window,似乎变量解析以某种方式与关联this,但事实并非如此。一旦您开始将其他东西用于thisthis可变分辨率就会完全彼此断开。