为什么赋值运算符返回一个值而不是一个引用?

11

我在这个网站上看到下面的例子,认为两个答案都应该是20而不是返回的10。他说逗号和赋值都返回一个值,而不是一个引用。我不太理解这意味着什么。

我知道它与将变量传递到函数或方法有关,即原始类型按值传递,对象按引用传递,但我不确定它在这种情况下如何应用。

我也了解到关于上下文和“this”的价值(从stackoverflow的帮助中),但我认为在这两种情况下我仍然会将其作为方法调用,foo.bar(),这意味着foo是上下文,但似乎两者都导致函数调用bar()。

为什么会这样,这一切又意味着什么?

var x = 10;
var foo = {
  x: 20,
  bar: function () {return this.x;}
};

(foo.bar = foo.bar)();//returns 10
(foo.bar, foo.bar)();//returns 10

1
(foo.bar = foo.bar)() 真的是我面试问题清单上的热门问题!^.^ - Ben Blank
@Ben:不是(foo.bar, foo.bar)()吗?;-) 我的意思是,如果你想要玄学的... - T.J. Crowder
11
请告诉我你是为哪家公司工作的,这样我就不会错误地来面试了 :) - screenm0nkey
4个回答

12
这与值和引用无关,与“this”值有关(正如您所怀疑的那样)。在JavaScript中,“this”是完全由函数被调用的方式而不是定义它的位置来设置的。您可以通过以下三种方式之一设置“this”值:
  1. 使用属性访问符表示法(即点号(obj.foo())或方括号表示法(obj ["foo"]()))通过对象属性调用函数。
  2. 使用“with”语句通过对象属性调用函数(实际上只是#1的一种变体,但值得单独提出,尤其是从源代码中不明显的情况下)
  3. 使用函数实例的“apply”或“call”功能。
在上面的示例中,您没有执行任何这些操作,因此最终使用默认的“this”值即全局���象调用该函数,因此“x”来自全局对象而不是您的“ foo”对象。以下是另一种思考该代码正在执行的方式:
var f = foo.bar; // Not calling it, getting a reference to it
f();             // Calls the function with `this` referencing the global object

如果您没有直接使用属性来实际调用函数(而是检索属性的,然后使用该值进行调用),那么this处理不会启动。


1
这与引用类型有关,因为逗号运算符和简单赋值都在内部使用GetValue,这导致引用的基对象丢失。 - Christian C. Salvadó
@CMS:是的,那就是我所描述的行为背后的基本机制。我认为深入探讨这些细节并不是必要的。(我也没提到[[Call]]方法或[[Scope]]属性 :-) )。 - T.J. Crowder
@TJ&CMS-技术上,您可以自由地回答。 如果我不理解,我会问问题。 同时,CMS的回答对我来说更有意义,因为我仍然无法理解为什么(foo.bar = foo.bar)()不等于foo.bar()。 在您的示例中,您仅使用了f = foo.bar,这对我很有意义,因为f()是一个函数调用,但如果f.bar = foo.bar,那么对我来说,它等同于f.bar(),这意味着f将是'this'。 - screenm0nkey
@Nick:(foo.bar = foo.bar)()的关键是要记住你正在调用表达式的结果。那个表达式的结果是什么?没错,一个函数引用——完全与任何属性解绑,就像(f = foo.bar)()一样。你不必深入了解它的内部机制,这是关键的部分。如果你了解,CMS已经为你提供了起点——但没有什么比阅读规范(http://www.ecma-international.org/publications/standards/Ecma-262.htm, §8.7)更好的了。不过,这很费劲;除非你真正学习它,否则你可能会变得更加困惑,而不是更少。 :-) - T.J. Crowder
@TJ - 我知道什么让我困惑了,就是在第一个foo和bar之间的点符号表示法。我很难理解如何让foo.bar等于只有bar(),当bar是foo的属性时。话虽如此,你的示例(f = foo.bar = foo.bar)()帮了我不少忙,我会在完全理解发生的事情之前使用它。因此,作为另一个荒谬的例子(nick.foo.low.man = foo.man.tan.bar)()是否仍然等于bar()? - screenm0nkey
显示剩余4条评论

8

您应该了解内部引用类型的工作原理。

注意: 这不是一种语言数据类型,而是处理引用的内部机制。

引用由两个元素组成,即基本对象属性名称

在您的示例中,foo.bar引用看起来像这样。

// pseudo-code
foo.bar = {
  baseObject: foo,
  propertyName: 'bar'
}

这两个操作符逗号运算符简单赋值都依赖于获取属性名称的值,导致基本对象丢失,因为只返回一个值(这是通过内部GetValue操作实现的)。

这就是内部GetValue操作的工作原理:

// pseudo-code
GetValue(V) :
  if (Type(V) != Reference) return V;

  baseObject = GetBase(V); // in your example foo
  if (baseObject === null) throw ReferenceError;

  return baseObject.[[Get]](GetPropertyName(V)); 
  // equivalent to baseObject[v.PropertyName];

如您所见,返回了一个值,因此原始引用丢失了。
编辑:理解为什么`(foo.bar = foo.bar)();`不等同于`foo.bar();`的关键在于简单赋值运算符,让我们看一下算法:
11.13.1 简单赋值(`=`) 产生式`AssignmentExpression`: `LeftHandSideExpression` = `AssignmentExpression`
评估如下:
1. 评估 `LeftHandSideExpression`。
2. 评估 `AssignmentExpression`。
3. 调用 `GetValue(Result(2))`。
4. 调用 `PutValue(Result(1), Result(3))`。
5. 返回 `Result(3)`。
基本上,当你执行`(foo.bar = foo.bar)`时,实际分配(步骤4)没有任何效果,因为`PutValue`只会获取引用的值并将其放回,使用相同的基对象。
关键在于赋值运算符返回(步骤5)步骤3中获取的值,就像我之前在GetValue伪代码中所说的那样,这个内部方法返回一个没有真正的基础对象

那会让我明白的。我曾经对T.J. Crowder说过:“你可以尽情地使用技术术语”,但现在我却无法理解它。如果可以的话,我需要思考一下你所写的内容,然后再回来找你。 - screenm0nkey
@Nick,好的,请考虑一下,我们正在处理一个完全抽象的类型,在规范中仅用于阐述目的。这可能会使理解变得有点困难。请阅读有关GetBaseGetPropertyNameGetValue内部方法的内容,然后我们可以进一步讨论,我可以解释一下引用类型在进行函数调用时如何用于设置this值。 - Christian C. Salvadó
谢谢你。你对JavaScript的理解非常出色。我会把你说的话记在心里,并进行一些研究来加深理解。此时此刻,我必须选择正确的答案并颁发给TJ,这并不是因为他的答案更好,而是因为只能选择一个。我会+1你和其他人的回答,因为我认为它们都很好,有帮助。再次感谢你的帮助。 - screenm0nkey
@Nick - 不用谢,尽量理解,如果你在学习过程中遇到任何问题,请随时问我!;) - Christian C. Salvadó

2

你误解了。

这两个示例都返回 windowx 属性,因为它们没有直接在 foo 上调用。

this 关键字的值取决于函数被调用的上下文。

在普通函数调用中(例如,myFunc()),this 将是全局对象,通常是 window
在对象方法调用中(例如,foo.bar()),this 将是调用该函数的对象。 (在本例中是 foo

您可以通过调用 myFunc.call(context, arg1, arg2)myFunc.apply(context, argArray) 显式设置上下文。

您的两个示例都是对评估为 foo.bar 的表达式的正常调用。
因此,thiswindow

它们是等效的

var func = (some expression);
func();

我理解你写的所有内容,包括你的例子,但在我的脑海中 (foo.bar = foo.bar) 仍然等同于 foo.bar() 而不是像你告诉我那样是 bar()(我知道这是正确的)。你、CMS 和 TJ 都给了我很好的回答,但我需要理解清楚,因为我有点简单。 - screenm0nkey

2

可以将点运算符的行为类比为with语句。当您运行foo.bar()时,结果与运行以下代码几乎相同:

with (foo) {
    bar(); // returns 20
}

这将使用全局对象上覆盖的foo运行您的bar函数,使其能够找到foo中的x,从而返回20。

如果您不立即调用bar,例如(foo.bar = foo.bar),则会得到:

with (foo) {
    bar; // returns "bar" itself
}

结果将从括号中传递出去,生成一个类似于<reference to bar>()的中间语句,它没有点运算符,因此没有with语句,所以无法访问foo,只能访问全局变量x
(当然,点运算符实际上并没有转换成with语句,但行为类似。)

谢谢,这对我很有帮助。我以前从未使用过with语句,但它给了我一些研究的东西。因此,在这个例子中(nick.foo.low.man = foo.man.tan.bar)(),它与以下代码相同:with (foo.man.tan) { bar; // 返回 "bar" 本身 } - screenm0nkey

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