似乎你已经理解得很好了(我对你下面的术语有一点小问题)。构造函数内的局部变量只是构造函数内的局部变量,它们根本不是由构造函数初始化的实例的一部分。
这都是 JavaScript 中“作用域”的结果。当你调用一个函数时,会为该函数调用创建一个“执行上下文”(EC)。EC 有一个称为“变量上下文”的东西,其中有一个“绑定对象”(让我们称之为“变量对象”,好吗?)。变量对象保存在函数内定义的所有 var 和函数参数等内容。这个变量对象是非常真实和非常重要的闭包工作方式,但你不能直接访问它。构造函数中的 x 是为调用构造函数创建的变量对象的属性。
所有作用域都有一个变量对象;神奇的是,全局作用域的变量对象是全局对象,在浏览器中是
window
。(更准确地说,
window
是变量对象上的一个属性,指向变量对象本身,因此可以直接引用它。函数调用中的变量对象没有任何等效的属性。) 因此,在全局作用域中定义的
x
是
window
的属性。
我承诺的术语细节:你说过:
如果调用全局函数,则会将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
或者你可以使用函数对象的 call
或 apply
特性来显式地设置 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";
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();
display("[foo] alpha = " + alpha);
display("[foo] beta = " + beta);
display("[foo] gamma = " + gamma);
setTimeout(callback, 200);
function callback() {
var alpha = "I'm alpha in the variable object for the call to `callback`";
newSection();
display("[callback] alpha = " + alpha);
display("[callback] beta = " + beta);
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
函数结束后仍然存在(foo
在setTimeout
调用callback
之前返回)。这就是闭包的工作原理。当一个函数被创建时(注意,每次调用foo
都会创建一个新的callback
函数对象),它会获得对那个时刻顶部作用域链中的变量对象的持久引用(整个变量对象,不仅仅是我们看到它引用的那些部分)。因此,在等待两个setTimeout
调用发生的短暂时刻内,我们在内存中有两个调用foo
的变量对象。还要注意,函数参数的行为与var
完全相同。以下是上述内容的运行时详细说明:
- 解释器创建全局作用域。
- 它创建全局对象并使用其默认属性集(
window
、Date
、String
和所有其他您习惯拥有的“全局”符号)来填充它。
- 它为全局范围内的所有
var
语句在全局对象上创建属性;最初它们的值为undefined
。所以在我们的例子中,是alpha
和beta
。
- 它为全局范围内所有函数声明在全局对象上创建属性;最初它们的值为
undefined
。所以在我们的例子中,是foo
和我实用程序函数display
和newSection
。
- 它按顺序处理全局范围内的每个函数声明:
- 创建函数对象
- 将其分配给当前变量对象的引用(在本例中为全局对象)
- 将函数对象分配给其在变量对象上的属性(再次,在本例中为全局对象)
- 解释器开始执行逐步代码,从顶部开始。
- 它到达的第一行是
var alpha = "I'm window.alpha";
。当然,它已经完成了这个var
方面的工作,所以它将其处理为直接赋值。
var beta = ...
也是如此。
- 它两次调用
display
(省略细节)。
foo
函数声明已经被处理过了,并且根本不是逐步代码执行的一部分,因此解释器到达的下一行是foo("I'm gamma1, passed as an argument to foo");
。
- 它为调用
foo
创建执行上下文。
- 它为此执行上下文创建变量对象,为方便起见,我将其称为
foo#varobj1
。
- 它将
foo#varobj1
分配一个副本,该副本是foo
对创建foo
的变量对象的引用(在本例中为全局对象);这是它与“作用域链”的链接。
- 解释器为
foo#varobj1
中所有命名的函数参数、var
和foo
内部的函数声明创建属性。所以在我们的例子中,那是gamma
(参数)、alpha
(var
)和callback
(声明的函数)。最初它们的值为undefined
。(这里还创建了一些其他默认属性,我不会详细介绍。)
- 它将函数参数的属性分配给传递给函数的值。
- 它按顺序处理
foo
中的每个函数声明(从头到尾)。在我们的例子中,那是callback
:
- 创建函数对象
- 将该函数对象分配给当前变量对象的引用(
foo#varobj1
)
- 将函数对象分配给其在
foo#varobj1
上的属性
- 解释器开始逐步执行
foo
代码
-
哎呀,这太有趣了,不是吗?
关于上述内容的最后一点:请注意 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
中定义的alpha
对bar
没有任何影响,尽管我们是从foo
中调用了bar
。让我们快速地回顾一下:
- 设置全局执行上下文等等。
- 为
var
和声明的函数创建属性。
- 将
alpha
赋值。
- 创建
foo
函数对象,给它一个对当前变量对象(即全局对象)的引用,并将其放在foo
属性上。
- 创建
bar
函数对象,给它一个对当前变量对象(即全局对象)的引用,并将其放在bar
属性上。
- 通过创建执行上下文和变量对象来调用
foo
。变量对象foo#varobj1
得到了foo
对其父变量对象的引用的副本,该父变量对象当然是全局对象。
- 开始逐步执行
foo
的代码。
- 查找自由变量
bar
,在全局对象中找到。
- 为调用
bar
及其关联的变量对象bar#varobj1
创建执行上下文。将bar#varobj1
分配一个对其父变量对象的引用的副本,该父变量对象当然是全局对象。
- 开始逐步执行
bar
的代码。
- 查找自由变量
alpha
:
- 首先在
bar#varobj1
上查找,但没有该名称的属性。
- 因此它查找下一个链接,即从
bar
获取的链接,即全局对象。所以它找到了全局的alpha
。
请注意,foo#varobj1
与bar
的变量对象没有任何关联。这是好事,因为如果作用域由函数被调用的方式和位置定义,我们都会疯掉的。 :-) 一旦你理解它与函数创建相关联,而函数创建受源代码的嵌套所决定,就会更容易理解。
这就是为什么bar
的作用域完全由它在源代码中的位置决定,而不是运行时如何调用。
一开始你可能会想到this
和变量解析之间的关系,这并不奇怪,因为在JavaScript中全局对象(window
)有两个无关的作用:1. 如果函数没有以设置不同的值的方式被调用(在全局范围内),它是默认的this
值,2. 它是全局变量对象。这些是解释器使用全局对象的无关方面,这可能会令人困惑,因为当this
=== window
时,似乎变量解析与this
有关,但实际上并不是这样。一旦你开始使用其他东西来代替this
,this
和变量解析就完全脱钩了。