在这个代码中发生了什么事情,Number对象持有属性并递增数字?

最近的一则鸣叫包含了这段JavaScript代码。

有人可以一步一步解释发生了什么吗?

> function dis() { return this } undefined > five = dis.call(5) Number {[[PrimitiveValue]]: 5} > five.wtf = 'potato' "potato" > five.wtf "potato" > five * 5 25 > five.wtf "potato" > five++ 5 > five.wtf undefined > five.wtf = 'potato?' "potato?" > five.wtf undefined > five 6 

特别是,我不清楚:

  • 为什么dis.call(5)的结果是某种[[PrimitiveValue]]属性的数字,但five++five * 5看起来只是普通数字525 (不是Number s)
  • 为什么five.wtf属性在five++增量后消失
  • 为什么five.wtf属性在five++增量之后不再可设置,尽pipe有five.wtf = 'potato?' 显然设定的价值。

OP在这里。 有趣的看到这堆栈溢出:)

在介绍行为之前,重要的是澄清一些事情:

  1. 数值和数字对象 ( a = 3 vs a = new Number(3) )是非常不同的。 一个是原始的,另一个是对象。 您不能将属性分配给基元,但可以将其分配给对象。

  2. 两者之间的强制是隐含的。

    例如:

     (new Number(3) === 3) // returns false (new Number(3) == 3) // returns true, as the '==' operator coerces (+new Number(3) === 3) // returns true, as the '+' operator coerces 
  3. 每个expression式都有一个返回值。 当REPL读取并执行一个expression式时,这就是它显示的内容。 返回值通常并不意味着你的想法和暗示的事情是不正确的。

好的,我们走吧。

JavaScript代码的原始图像

誓言。

 > function dis() { return this } undefined > five = dis.call(5) [Number: 5] 

定义一个函数dis并用5 调用它。 这将执行5作为上下文( this )的function。 在这里它被从一个数字值强制转换为一个数字对象。 这是非常重要的,要注意的是,我们在严格的模式, 这不会发生

 > five.wtf = 'potato' 'potato' > five.wtf 'potato' 

现在我们把属性five.wtf设置为'potato' ,用五个作为对象,当然它接受简单分配 。

 > five * 5 25 > five.wtf 'potato' 

five作为对象,我确保它仍然可以执行简单的算术运算。 它可以。 它的属性仍然坚持? 是。

轮到了。

 > five++ 5 > five.wtf undefined 

现在我们检查five++ 。 后缀增量的技巧是整个expression式将评估原始值,然后增加值。 看起来five还是五个,但是真正的performance评价为五个,然后是five6

five不仅被设置为6 ,而且被强制回到一个数字值,所有的属性都丢失了。 由于原语不能保存属性,因此five.wtf是未定义的。

 > five.wtf = 'potato?' 'potato?' > five.wtf undefined 

我再次试图重新分配一个属性wtffive 。 返回值意味着它坚持,但实际上并不是因为five是一个数值,而不是一个数字对象。 这个expression评价为'potato?' ,但是当我们检查我们看到它没有分配。

威望。

 > five 6 

自从后缀增量以来, five已经是6

有两种不同的方法来表示一个数字:

 var a = 5; var b = new Number(5); 

第一个是原始的,第二个是对象。 对于所有的意图和目的,两者的行为都是相同的,只是在打印到控制台时看起来不同。 一个重要的区别是,作为一个对象, new Number(5)接受新的属性,就像任何普通的{} ,而基元5不会:

 a.foo = 'bar'; // doesn't stick b.foo = 'bar'; // sticks 

至于最初的dis.call(5)部分,请看“this”这个关键字是如何工作的? 。 我们只需要说第一个call参数被用作这个值,而且这个操作会把这个数字强制为更复杂的Number对象forms。*稍后++强制它回到原始forms,因为加法操作+导致一个新的原始。

 > five = dis.call(5) // for all intents and purposes same as new Number(5) Number {[[PrimitiveValue]]: 5} > five.wtf = 'potato' "potato" > five.wtf "potato" 

Number对象接受新的属性。

 > five++ 

++导致一个新的原始值6

 > five.wtf undefined > five.wtf = 'potato?' "potato?" > five.wtf undefined 

…没有和不接受自定义属性。

*请注意,在严格模式下this参数将被区别对待, 不会被转换为Number 。 有关实现细节,请参阅http://es5.github.io/#x10.4.3

JavaScript世界中的强制 – 侦探故事

纳森,你不知道你发现了什么。

我已经调查了几个星期了。 这一切都是在去年十月的暴风雨之夜开始的。 我意外地偶然发现了Number类 – 我的意思是,为什么在JavaScript中有一个Number类?

我没有准备好接下来要发现的内容。

事实certificate,没有告诉你,JavaScript已经改变了你的号码,把对象和你的对象改变成你鼻子下面的号码。

JavaScript希望没有人会接受,但人们一直在报告奇怪的意外行为,现在感谢你和你的问题,我有我需要的证据来打开这个事情。

这是我们迄今为止发现的。 我不知道我是否应该告诉你这一点 – 你可能想closures你的JavaScript。

 > function dis() { return this } undefined 

当你创build这个函数时,你可能不知道接下来会发生什么。 一切都很好,现在一切都很好。

没有错误消息,只是在控制台输出中的“未定义”一词,正是你所期望的。 毕竟,这是一个函数声明 – 它不应该返回任何东西。

但这只是一个开始。 接下来发生的事情,谁都没有预料到。

 > five = dis.call(5) Number {[[PrimitiveValue]]: 5} 

是的,我知道,你预计5 ,但这不是你得到的,是吗 – 你有别的东西 – 有些不同。

这样的事情我也经历过。

我不知道该怎么做。 这让我疯狂。 我睡不着,我不能吃,我试着把它喝掉,但没有任何的山露会让我忘记。 这只是没有任何意义!

那时候我才知道发生了什么事情 – 这是强制性的,发生在我眼前,但是我太盲目了。

Mozilla试图把它放在他们知道没有人会看的地方 – 他们的文档 。

经过几个小时的recursion阅读,重新阅读和重新阅读,我发现这个:

“…原始值将被转换为对象。”

这是正确的,可以用Open Sans字体拼出来。 这是call()函数 – 我怎么会这么愚蠢?!

我的电话号码不再是一个号码。 我把它传递给call()的那一刻,变成了别的东西。 它变成了一个对象。

起初我不敢相信。 这怎么可能是真的? 但我无法忽视周围的证据。 如果你只是看:

 > five.wtf = 'potato' "potato" > five.wtf "potato" 

wtf是对的。 数字不能有自定义属性 – 我们都知道! 这是他们在学院教你的第一件事。

我们应该知道我们看到控制台输出的那一刻 – 这不是我们认为的数字。 这是一个骗子 – 一个物体,作为我们甜美无辜的数字。

这是… new Number(5)

当然! 这非常有意义。 call()有一个工作要做,他不得不调用一个函数,为此他需要填充this ,他知道他不能用一个数字来做到这一点 – 他需要一个对象,他愿意做任何事情得到它,即使这意味着强迫我们的数字。 当call()看到数字5 ,他看到了一个机会。

这是一个完美的计划:等到没有人看,把我们的号码换成一个看起来就像它的物体。 我们得到一个数字,函数被调用,没有人会更聪明。

这确实是一个完美的计划,但是就像所有的计划,甚至是完美的计划一样,它里面有一个漏洞,我们即将陷入困境。

看,什么call()不明白的是,他不是城里唯一可以强制数字的人。 毕竟这是JavaScript – 强制无处不在。

call()把我的号码,我不会停止,直到我把他的小骗子的面具,并暴露给整个堆栈溢出社区。

但是,如何? 我需要一个计划。 当然,它看起来像一个数字,但我知道它不是,有一种方法来certificate这一点。 而已! 它看起来像一个数字,但它可以像一个?

我告诉five我需要他成为5倍 – 他没有问为什么,我没有解释。 然后,我做了一个好程序员会做的事情:我成倍增长。 当然,他没有办法摆脱这一切。

 > five * 5 25 > five.wtf 'potato' 

该死的! 不仅five只繁殖蛮好的还在那里。 该死的人和他的马铃薯。

到底是怎么回事? 我是否错了这件事? five真的是一个数字吗? 不,我必须错过一些东西,我知道,有一些我必须遗忘,一些如此简单和基本的东西,我完全忽略了它。

这看起来不太好,我已经写了几个小时的这个答案,我还没有接近我的观点。 我不能保持这种状态,最终人们会停止阅读,我不得不想一些事情,我必须快速思考。

等等就是这样! five不是25,25是结果,25是一个完全不同的数字。 当然,我怎么能忘记? 数字是不可变的。 当你乘以5 * 5什么都没有分配给任何东西,你只是创build一个新的数字25

这一定是这里发生的事情。 不知何故,当我乘以five * 5five必须被强制为一个数字,这个数字必须是用于乘法的数字。 这是打印到控制台的乘法结果,而不是five本身的值。 five从来没有分配任何东西 – 所以当然不会改变。

那么我怎样才能得到five分配自己的手术的结果。 我知道了。 在five还没有机会思考之前,我喊了一声“++”。

 > five++ 5 

啊哈! 我有他! 每个人都知道5 + 16 ,这是我需要揭露的five证据不是数字的证据! 这是一个骗子! 一个坏的冒名顶替者,不知道如何计数。 我可以certificate这一点。 以下是一个真实的数字如何行事:

 > num = 5 5 > num++ 5 

等待? 这里发生了什么? 口气,我陷入了困境,我忘记了邮政经营者的工作方式。 当我在five的末尾使用++时,我说的是返回当前值,然后递增five 。 这是操作发生之前的值被打印到控制台。 实际上6我可以certificate的:

 >num 6 

时间看看真正的five是什么:

 >five 6 

这正是它应该的。 five是好的 – 但我更好。 如果five仍然是一个对象,这意味着它仍然有财产wtf ,我愿意赌一切它没有。

 > five.wtf undefined 

啊哈! 我是对的。 我有他! five是现在的数字 – 这不再是一个对象。 我知道乘法技巧这次不会挽救它。 看five++实际上是five = five + 1 。 与乘法不同, ++运算符将值赋给five 。 更具体地说,它指定了five + 1的结果,就像在乘法的情况下一样,返回一个新的不变的数字

我知道我有他,只是为了确保他无法摆脱它。 我还有一个testing了我的袖子。 如果我是对的,现在five确实是一个数字,那么这是行不通的。

 > five.wtf = 'potato?' 'potato?' 

这次他不会骗我的。 我知道potato? 将被打印到控制台,因为这是作业的输出。 真正的问题是, wtf还会在那里吗?

 > five.wtf undefined 

正如我怀疑 – 没有 – 因为数字不能被分配属性。 我们了解到学院的第一年;)

谢谢Nathan。 感谢你们提出这个问题的勇气,我终于可以把这一切都放在我身后,并转向一个新的案例。

像这个关于函数toValue() 。 哦亲爱的上帝 拿去!

 01 > function dis() { return this } 02 undefined 03 > five = dis.call(5) 04 Number {[[PrimitiveValue]]: 5} 05 > five.wtf = 'potato' 06 "potato" 07 > five.wtf 08 "potato" 09 > five * 5 10 25 11 > five.wtf 12 "potato" 13 > five++ 14 5 15 > five.wtf 16 undefined 17 > five.wtf = 'potato?' 18 "potato?" 19 > five.wtf 20 undefined 21 > five 22 6 

01声明一个返回上下文对象的函数disthis代表的变化取决于你是否使用严格模式。 如果函数被声明为:整个示例有不同的结果:

 > function dis() { "use strict"; return this } 

这在ES5规范的第10.4.3节中有详细说明

  1. 如果函数代码是严格代码,则将ThisBinding设置为thisArg。
  2. 否则,如果thisArg为null或未定义,请将ThisBinding设置为全局对象。
  3. 否则,如果Type(thisArg)不是Object,则将ThisBinding设置为ToObject(thisArg)。

02是函数声明的返回值。 undefined在这里应该是自我解释的。

03在原始值5的上下文中调用时,variablesfive被初始化为dis的返回值。 因为dis不是严格模式,所以这一行和调用five = Object(5)是一样的。

04奇数Number {[[PrimitiveValue]]: 5}返回值是包装原始值5的对象的表示

05 five对象的wtf属性被分配一个string值'potato'

06是作业的返回值,应该是自我解释的。

07正在检查five对象的wtf属性

08 as five.wtf先前被设置为'potato' ,这里返回'potato'

09 five对象被乘以原始值5 。 这与其他任何对象相乘并没有什么不同,在ES5规范的第11.5节中有解释。 特别值得注意的是对象如何转换为数值,这在几个小节中已经介绍过了。

9.3 ToNumber :

  1. 设primValue为ToPrimitive(input参数,提示数字)。
  2. 返回ToNumber(primValue)。

9.1原创 :

返回对象的默认值。 通过调用对象的[[DefaultValue]]内部方法来检索对象的默认值,并传递可选的提示PreferredType。 本规范针对8.12.8中的所有本地ECMAScript对象定义了[[DefaultValue]]内部方法的行为。

8.12.8 [[DefaultValue]] :

让valueOf是用参数“valueOf”调用对象O的[[Get]]内部方法的结果。

  1. 如果IsCallable(valueOf)为true,那么,

    1. 让val是调用valueOf的[[Call]]内部方法的结果,其中O为此值和一个空的参数列表。
    2. 如果val是原始值,则返回val。

这是一个迂回的说法,这个对象的valueOf函数被调用,并且这个函数的返回值被用在方程中。 如果您要更改valueOf函数,则可以更改操作的结果:

 > five.valueOf = function () { return 10 } undefined > five * 5 50 

10作为five valueOf函数是不变的,它返回包装的原始值5以便five * 5评估到5 * 5 ,结果25

11 five对象的wtf属性再次被评估尽pipe与05被分配的时候没有变化。

12 'potato'

13 后缀增量运算符被调用five ,得到数值( 5 ,我们介绍了如何更早),存储的价值,使它可以返回,加1到值( 6 ),赋值为five ,和返回存储的值( 5

如前所述,返回的值是增加前的值

存储在variablesfive中的原始值( 6 )的wtf属性被访问。 ES5规范的第15.7.5节定义了这种行为。 Numbers从Number.prototype获取属性。

Number.prototype没有一个wtf属性,所以返回undefined

17 five.wtf被分配了一个'potato?'的值。 。 分配在ES5规范的11.13.1中定义 。 基本上分配的值是返回,但不存储。

18 'potato?' 被赋值运算符返回

19再次five ,其值为6被访问,并且Number.prototype没有一个wtf属性

20如上所述undefined

21 five访问

13返回22 6

这很简单。

 function dis () { return this; } 

这返回this上下文。 所以,如果你call(5)你传递的数字作为一个对象。

call函数不提供参数,你给的第一个参数是这个的上下文。 通常情况下,如果你想在上下文中使用它,你可以使用dis.call({}) ,这意味着在函数中this是一个空的。 但是,如果你传递5它似乎将被转换为一个对象。 参见.call

所以回报是object

当你做five * 5 ,JavaScript将对象five看作原始types,所以相当于5 * 5 。 有趣的是, '5' * 5 ,它仍然等于25 ,所以JavaScript显然是在引擎盖下。 这一行没有对底层的fivetypes进行更改

但是,当你做++它会将对象转换为原始numbertypes,从而删除.wtf属性。 因为你正在影响基础types

原始值不能有属性。 但是,当您尝试访问原始值上的属性时,它会透明地转换为临时Number对象。

所以:

 > function dis() { return this } undefined // Like five.dis(), so dis return the temporaty Number object and // reference it in five > five = dis.call(5) Number {[[PrimitiveValue]]: 5} // Write the wtf attribut on the Number object referenced by five > five.wtf = 'potato' "potato" // Read the wtf attribut on the Number object referenced by five > five.wtf "potato" // Return 5*5 but dont change the reference of five > five * 5 25 // Read the same wtf attribut on the Number object referenced by five > five.wtf "potato" // Change the five reference to a new primitive value (5+1). Five // reference a primitive now. > five++ 5 // Read the wtf attribut on a new temporary Number object construct from // the primitive referenced by five. So wtf does not exist. > five.wtf undefined // Write the wtf attribut on a new temporary Number object construct from // the primitive referenced by five. But this object not referenced by // five. It will be lost. > five.wtf = 'potato?' "potato?" // Read the wtf attribut on a new temporary Number object construct from // the primitive referenced by five. So wtf does not exist. > five.wtf undefined > five 6 

声明函数dis 。 函数返回其上下文

 function dis() { return this } undefined 

调用与上下文5 。 在严格模式( MDN )中作为上下文传递时,原始值被装箱。 所以five现在是对象(盒装数字)。

 five = dis.call(5) Number {[[PrimitiveValue]]: 5} 

声明fivevariables的wtf属性

 five.wtf = 'potato' "potato" 

five.wtf

 five.wtf "potato" 

five是盒装5 ,所以它的数量和对象在同一时间(5 * 5 = 25)。 它不会改变five

 five * 5 25 

five.wtf

 five.wtf "potato" 

在这里拆箱five five现在只是原始的number 。 它打印5 ,然后添加1five

 five++ 5 

five是现在的原始数字6 ,没有任何属性。

 five.wtf undefined 

基元不能有属性,你不能设置这个

 five.wtf = 'potato?' "potato?" 

你不能读这个,因为它没有设置

 five.wtf undefined 

five是因为岗位递增以上

 five 6 

首先,它看起来像是通过nodejs控制台运行。

1。

  function dis() { return this } 

创build函数dis(),但是因为它没有被设置为var所以没有值返回,因此即使dis()被定义,输出也是未定义的。 在旁注中, this没有被返回,因为该函数没有被执行。

2。

  five = dis.call(5) 

这将返回javascript的Number对象,因为您只需将函数dis()this值设置为基元5。

3。

  five.wtf = 'potato' 

第一个返回"potato"因为你只是将属性wtf设置为'potato' 。 Javascript返回你设置的variables的值,可以很容易地链接多个variables,并将它们设置为相同的值,如下所示: a = b = c = 2

4。

  five * 5 

这会返回25因为您只是将原始数字5乘以fivefive的值是由Number对象的值决定的。

5。

  five.wtf 

之前我跳过这一行,因为我会在这里重复它。 它只是返回上面设置的属性wtf的值。

6。

  five++ 

正如@Callum所说的, ++会将types转换为对象Number {[[PrimitiveValue]]: 5}}相同的值。

现在因为five是一个number ,你不能再设置属性,直到你做这样的事情:

  five = dis.call(five) five.wtf = "potato?" 

要么

  five = { value: 6, wtf: "potato?" } 

另请注意,第二种方法与使用第一种方法的行为不同,因为它定义了一个通用对象,而不是之前创build的Number对象。

我希望这可以帮助,JavaScript喜欢假设的事情,所以当从Number对象改变为原始number时,可能会变得混乱。 您可以使用typeof关键字来检查是什么types的东西,在初始化它之后写入typeof返回'object' ,并且在您执行five++后返回'number'

@deceze非常好地描述了Number对象和原始数字之间的区别。

JavaScript范围由执行上下文组成。 每个执行上下文都有一个词法环境(外部/全局范围的值),一个variables环境(本地范围的值)和一个这个绑定

这个绑定是执行上下文中非常重要的一部分。 使用call是一种方法来改变这个绑定 ,这样做会自动创build一个对象来填充绑定。

Function.prototype.call() (来自MDN)

句法
fun.call(thisArg[, arg1[, arg2[, ...]]])

thisArg
这个提供给这个调用fun的值。 请注意,这可能不是该方法看到的实际值:如果该方法是非严格模式代码中的函数,则将使用全局对象replacenull和undefined,并将原始值转换为对象 。 (重点是我的)

一旦显示5被转换成new Number(5) ,其余的应该是相当明显的。 请注意,只要它们是原始值,其他示例也将起作用。

 function primitiveToObject(prim){ return dis.call(prim); } function dis(){ return this; } //existing example console.log(primitiveToObject(5)); //Infinity console.log(primitiveToObject(1/0)); //bool console.log(primitiveToObject(1>0)); //string console.log(primitiveToObject("hello world")); 
 <img src="http://i.stack.imgur.com/MUyRV.png" /> 

几个概念解释发生了什么

5是一个数字,一个原始值

Number {[[PrimitiveValue]]: 5}是Number的一个实例(让我们称之为对象包装器)

每当你访问一个原始值的属性/方法时,JS引擎将创build一个适当types的对象包装器( Number for 5String 'str'Boolean for true ),parsing对象的属性access / method调用包装。 例如,当您执行true.toString()时会发生这种情况。

对对象执行操作时,它们被转换为原始值(通过使用toStringvalueOf )以解决这些操作 – 例如,在执行

 var obj = { a : 1 }; var string = 'mystr' + obj; var number = 3 + obj; 

string将持有mystrobj.toString()的string连接,并且number将保持3obj.valueOf()的加法。

现在把它放在一起

 five = dis.call(5) 

dis.call(5)行为就像(5).dis()如果5实际上有dis方法。 为了解决方法调用,将创build对象包装器,并在其上parsing方法调用。 此时五点指向原始值5的对象包装。

 five.wtf = 'potato' 

在一个对象上设置一个属性,这里没有什么特别的。

 five * 5 

这实际上是five.valueOf() * 5从对象包装器获取原始值。 five仍然指向最初的对象。

 five++ 

这实际上是five = five.valueOf() + 1在这行之前five包含值5的对象包装,而此后five持有原始值6

 five.wtf five.wtf = 'potato?' five.wtf 

five不再是一个对象。 每行都创build一个Number的新实例,以便parsing.wtf属性访问。 这些实例是独立的,因此在其中设置属性在另一个上不可见。 代码完全等同于这个:

 (new Number(6)).wtf; (new Number(6)).wtf = 'potato?'; (new Number(6)).wtf;