使用Number对象保存属性并递增数字的这段代码中发生了什么?


246

最近的一条推文包含此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)Number有某种的[[PrimitiveValue]]属性,但结果five++five * 5似乎只是普通号525(不Number为s)
  • 为什么five.wtf属性在five++增量后消失
  • 为什么尽管分配显然设置了该值,但在增加five.wtf之后该属性甚至无法设置。five++five.wtf = 'potato?'

6
哈哈,我看到那条推文!它是如此奇怪,我认为是因为您将其相乘,它不会完全影响对象,但++似乎会影响基础类型
Callum Linington

8
您不了解哪条线?PrePost增量?乘法之后,它又是一个Number不具有wtf属性的对象...但是仍然是一个对象,object因此它可以具有properties..
Rayon'July

25
当您使用调用disdis.call(5),这会将原始数字包装5Number包含的类型的对象中5,以便可以将该对象作为this对象返回。在++它回到不能包含性的原始数转换,所以wtf开始是未定义的。
Gserg '16


5
@Rayon ...而ES6已经改变了。因此,向前推进这件事将不再发生。
GSerg

Answers:


278

在这里操作。看到堆栈溢出很有趣:)

在逐步进行操作之前,重要的是要澄清一些事情:

  1. 数字值数字对象a = 3vs 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. 每个表达式都有一个返回值。当REPL读取并执行表达式时,它就是显示的内容。返回值通常并不表示您的想法,而是表示不正确的事情。

好的,我们开始。

JavaScript代码的原始图像

誓言。

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

定义一个函数dis并使用调用5。这将以5上下文(this)执行该函数。在这里,它从Number值强制转换为Number对象。需要特别注意的是,如果我们采用严格模式, 这种情况就不会发生

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

现在,将属性设置five.wtf'potato',并以5作为对象,确保它接受简单分配

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

随着five作为对象,我保证它仍然可以进行简单的算术运算。它可以。它的属性仍然存在吗?是。

转。

> five++
5
> five.wtf
undefined

现在我们检查five++后缀递增的技巧是整个表达式将根据原始值求值,然后递增该值。看起来five仍然是5,但实际上该表达式的计算结果为5,然后设置five6

不仅five设置为6,而且还被强制转换为Number值,并且所有属性均丢失。由于基本体无法保存属性,five.wtf因此未定义。

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

我再次尝试将属性重新分配wtffive。返回值表示它坚持,但实际上不是,因为five它是Number值,而不是Number对象。该表达式的计算结果为'potato?',但是当我们检查时发现它没有被赋值。

威望。

> five
6

自从后缀增加以来,five一直是6


70
您在密切关注吗?
Waddles

3
@Nathan Long Well,在Java中,JavaScript从一开始就大量借用了,所有原语都具有等效的类。intInteger,例如。我认为这样做是为了您可以创建一个函数doSomething(Object),并且仍然能够为其提供原语。在这种情况下,基元将被转换为其相应的类。但是JS并不真正关心的类型,所以原因可能是别的东西
Suppen

4
@Eric首先要做的++是将ToNumber应用于该值。将类似的情况与字符串进行比较:如果有x="5",则x++返回number 5
apsillers's

2
真的,所有的魔力都发生在dis.call(5),对对象的强迫下,我从没想到过。
mcfedr '16

3
@gman,除了有很多更详细的影响,包括其标准库大块的命名,标准对象类型的行为,甚至使用大括号和分号语法(即使分号是可选)源于它被设计为Java程序员熟悉的事实。是的,这使初学者感到困惑。这并不意味着不存在影响。
Jules'July

77

代表数字有两种不同的方式:

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”关键字如何工作?。我们只说to的第一个参数call用作的值this,并且此操作将数字强制转换为更复杂的Number对象形式。*稍后将其++强制返回原始形式,因为加法运算+会生成新的原始形式。

> 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


2
在C / C ++中,@ Pharap当然可以做得更好#define true false。或在Java中,您可以重新定义数字的含义。这些是好的固体语言。实际上,每种语言都有类似的“技巧”,您可以得到预期的效果,但看起来可能很奇怪。
VLAZ '16

1
@Vld好,让我改写一下,这就是为什么我讨厌鸭子打字。
法拉普

59

JavaScript世界中的强制性-侦探故事

内森,您不知道发现了什么。

我已经对此进行了数周的调查。一切始于去年十月的一个暴风雨之夜。我无意间偶然发现了Number该类-我的意思是,为什么世界上JavaScript都有一个Number类?

我没有为接下来要查找的内容做准备。

事实证明,JavaScript一直在不告诉您的情况下,将您的数字更改为对象,而您的对象更改为您的鼻子下面的数字。

JavaScript希望没有人能追上,但是人们一直在报告奇怪的意外行为,现在感谢您和您的问题,我有证据表明我需要对此事大肆宣扬。

到目前为止,这是我们发现的。我不知道我是否该告诉您-您可能要关闭JavaScript。

> function dis() { return this }
undefined

创建该函数时,您可能不知道接下来会发生什么。一切看起来都很好,一切都很好-现在。

没有错误消息,只是控制台输出中的单词“ undefined”,恰恰是您所期望的。毕竟,这是一个函数声明-不应返回任何内容。

但这仅仅是开始。接下来发生了什么,没人能预料到。

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

是的,我知道,您期望得到一个5,但是那不是您得到的,是-您得到了其他东西-有所不同。

这样的事情我也经历过。

我不知道该怎么做。它使我发疯。我无法入睡,无法进食,试图将其喝掉,但是没有多少山露会让我忘记。只是没有任何意义!

那是我发现真正发生的事情的时候-那是强制,它就在我眼前发生,但是我实在是太盲目看不到了。

Mozilla试图通过将其放置在他们知道没人看的地方来隐藏它-他们的文档

经过数小时的递归阅读,重新阅读和重新阅读,我发现了这一点:

“ ...,原始值将转换为对象。”

就在这里,很明显,可以用Open Sans字体拼写出来。这就是call()功能-我怎么会这么傻?

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

起初我不敢相信。这怎么可能是真的?但是我不能忽略围绕我的证据。只要看一下就在这里:

> five.wtf = 'potato'
"potato"

> five.wtf
"potato"

wtf是正确的。数字不能具有自定义属性-我们都知道这一点!这是他们在学院里教您的第一件事。

我们应该在看到控制台输出的那一刻就知道了-这不是我们认为的数字。这是一个冒名顶替者-一个物体冒充我们甜蜜的无辜数字。

这是... new Number(5)

当然!这是完全合理的。call()有一件工作要做,他必须调用一个函数,然后要做他需要填充的事情this,他知道自己不能用数字来做到这一点-他需要一个对象,并且他愿意做任何事情来获得它,甚至如果那意味着强迫我们的电话号码。当call()看到数字时5,他看到了机会。

这是一个完美的计划:等到没人看时,再用我们的电话换一个看起来像它的物体。我们得到一个数字,该函数被调用,没有人会更明智。

这确实是一个完美的计划,但是像所有计划一样,甚至是完美的计划,在其中也有一个漏洞,我们即将陷入其中。

瞧,call()不明白的是,他不是镇上唯一可以强迫数字的人。毕竟这是JavaScript-到处都是强制。

call() 拿走了我的电话号码,直到我将面具从他的小骗子身上拉下并将他暴露给整个Stack Overflow社区,我才停下来。

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

我告诉five我需要他变得大5倍-他没有问为什么,我也没有解释。然后,我做了任何优秀的程序员都会做的事情:我成倍增加。当然,他没有办法伪造自己的出路。

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

该死的!不仅five乘法wtf还不错。该死的家伙和他的土豆。

到底是怎么回事?我整个事情都错了吗?是five真的多少?不,我必须丢失一些东西,我知道,有些东西我必须忘记,这些东西非常简单和基本,以至于我完全忽略了它。

这看起来不太好,我已经写了好几个小时的答案,但我离提出观点还差得远。我无法坚持下去,最终人们会停止阅读,我不得不思考一些事情,并且必须快速思考。

等一下!five不是25,结果是25,25是一个完全不同的数字。当然,我怎么会忘记?数字是不可变的。当乘以5 * 5任何东西时,您将只创建一个新数字25

那一定是这里发生的事情。我以某种方式乘法时five * 5five必须将其强制为一个数字,并且该数字必须是用于乘法的数字。是乘积的结果被打印到控制台,而不是其five本身的值。five永远不会被分配任何东西-因此,它当然不会改变。

因此,我该如何five为自己分配操作结果。我知道了。之前five甚至有一个思考的机会,我大喊:“++”。

> five++
5

啊哈!我有他!大家都知道5 + 16,这是我需要公开的证据five是不是一个数字!那是骗子!一个糟糕的冒名顶替者,不知道如何计算。我可以证明这一点。这是实数的作用方式:

> num = 5
5
> num++
5

等待?这是怎么回事 叹息,我陷入了破产的困境,five以至于我忘记了邮政接线员的工作方式。当我++在末尾使用时,five是说要返回当前值,然后递增five。将操作发生之前的值打印到控制台。num实际上6,我可以证明这一点:

>num
6

是时候看看five真正的含义了:

>five
6

...这正是应该的。five很好-但是我更好。如果five仍然是一个对象,那意味着它仍然会拥有该属性,wtf而我愿意打赌它没有的一切。

> five.wtf
undefined

啊哈!我是对的。我有他!five现在是一个数字-它不再是一个对象。我知道这次乘法将无法保存。看five++是真的five = five + 1。与乘法不同,运算++符为分配一个值five。更具体地,分配给它的结果five + 1,其就像在乘法返回一个新的不可变的情况下

我知道我有他,只是为了确保他不会从他那儿弯腰。我又袖手旁观了。如果我是对的,并且five现在确实是一个数字,那么这将不起作用:

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

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

> five.wtf
undefined

就像我怀疑的那样-什么都没有-因为数字无法分配属性。我们是在学院第一年学到的;)

谢谢内森。多亏您提出这个问题的勇气,我终于可以把所有这些都抛在脑后,然后再提出一个新的案例。

像这样关于功能toValue()。哦,亲爱的上帝。不!


9
忘了杰克吧!这是Javascript。
赛斯

为什么我们在JS中将构造函数称为“类”?
Evolutionxbox

27
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声明一个dis返回上下文对象的函数。this代表什么变化取决于您是否使用严格模式。如果将该函数声明为:整个示例将得到不同的结果:

> 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使用原始值上下文中的when five的返回值初始化变量。由于不是处于严格模式,因此此行与call相同。dis5disfive = Object(5)

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

05five对象的wtf属性分配一个字符串值'potato'

06 是赋值的返回值,应该可以自我解释。

07five对象的wtf属性被检查

08five.wtf先前设置的那样'potato'返回'potato'此处

09five对象乘以原始值5。这与要相乘的任何其他对象没有什么不同,并在ES5规范的11.5节中进行了说明。需要特别注意的是如何将对象转换为数值,这将在几节中介绍。

9.3 ToNumber

  1. 令primValue为ToPrimitive(输入参数,提示编号)。
  2. 返回ToNumber(primValue)。

9.1 ToPrimitive

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

8.12.8 [[DefaultValue]]

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

  1. 如果IsCallable(valueOf)为true,

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

这就是说要valueOf调用对象的函数并在方程式中使用该函数的返回值的一种round回方式。如果要更改valueOf功能,则可以更改操作结果:

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

10由于fives valueOf函数未更改,因此它返回包装的原始值,5以便five * 5计算5 * 5得出的结果25

11five对象的wtf属性被评估一次,尽管已经从当它被分配上保持不变05

12 'potato'

13后缀递增操作上调用five,它得到的数值(5,我们讨论了如何更早),专卖店这样的价值,它可以返回,增加1的值(6),并将值five,并返回存储的值(5

14 和以前一样,返回的值是递增之前的值

15访问存储在变量wtf中的原始值(6)的属性fiveES5规范的第15.7.5节定义了此行为。数字从中获取属性Number.prototype

16 Number.prototype没有wtf属性,因此undefined返回

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

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

19再次访问,five其值为6,再次Number.prototype没有wtf属性

20 undefined 如上所述

21 five 被访问

22 6 按照说明返回 13


17

很简单

function dis () { return this; }

这将返回this上下文。因此,如果您这样做,则将call(5)数字作为对象传递。

call函数不提供参数,您提供的第一个参数是的上下文this。通常,如果您希望在上下文中使用它,则可以{}这样指定dis.call({}),这意味着this该函数为空this。但是,如果通过,5它似乎将被转换为对象。参见.call

所以回报是 object

执行此操作时five * 5,JavaScript会将对象five视为原始类型,因此等效于5 * 5。有趣的是,do '5' * 5仍然等于25,因此JavaScript显然可以在后台进行转换。此行未对基础five类型进行任何更改

但是,当您执行++此操作时,它将把对象转换为原始number类型,从而删除该.wtf属性。因为您正在影响基础类型


返回值为Number
GSerg

++转换并分配回去。++等于variable = variable + 1。所以你松了wtf
Rajesh

*VS ++根本不会混淆我。有一个不希望有参数且不会返回的this函数,让该函数无论如何都接受一个参数并返回类似但不完全是参数的值,这对我来说毫无意义。
内森·朗

@NathanLong更新了它以显示dis.call()功能。
卡勒姆·利宁顿

5 ++的行为与C语言相同,因此不足为奇。this只是指向对象的指针,因此隐式转换原始类型。为什么没有参数的函数至少没有上下文?call或的第一个参数bind用于设置上下文。另外,函数是闭包,这意味着它们不仅可以访问arguments
Rivenfall '16

10

基本值不能具有属性。但是,当您尝试访问原始值的属性时,它会透明地转换为临时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

8

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

function dis() { return this }
undefined

调用dis上下文5。当在严格模式(MDN)中将其作为上下文传递时,将原始值装箱。因此,five现在是对象(框号)。

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

声明变量的wtf属性five

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

five6现在是原始编号,其中没有属性。

five.wtf
undefined

基本体不能具有属性,您不能设置此属性

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

您无法阅读,因为未设置

five.wtf
undefined

five6由于职位增加以上

five
6

7

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

1。

    function dis() { return this }

创建函数dis(),但由于未将其设置为a var,因此undefined即使dis()已定义,也没有返回值,因此输出也没有。在一个旁注中,this由于未执行功能而未返回。

2。

    five = dis.call(5)

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

3。

   five.wtf = 'potato'

第一个返回"potato",因为你设置的属性wtffive'potato'。Javascript返回您设置的变量的值,从而可以轻松地链接多个变量并将它们设置为相同的值,如下所示:a = b = c = 2

4。

    five * 5

这种回报25,因为你只是乘以原始号码5five。的值fiveNumber对象的值确定。

5,

    five.wtf

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

6。

    five++

就像@Callum所说的,++将把type转换成number来自object的相同值Number {[[PrimitiveValue]]: 5}}

现在,因为fivenumber,所以不能再为其设置属性,除非您执行以下操作:

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

要么

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

还要注意,第二种方法与使用第一种方法会有不同的行为,因为它定义的是通用对象,而不是Number之前创建的对象。

我希望这会有所帮助,javascript喜欢假设事物,因此在从Number对象更改为基本类型时会引起混淆number。您可以检查是什么类型的东西,通过使用typeof关键字,写的typeof 5初始化后返回'object',并在此之后five++返回'number'

@deceze很好地描述了Number对象和原始编号之间的区别。


6

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

此绑定是执行上下文的一个非常重要的组成部分。使用call是更改此绑定的一种方法,并且这样做会自动创建一个对象来填充绑定。

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

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

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

一旦很明显将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" />

在此处输入图片说明


4

几个概念解释发生了什么

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

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

每当您访问原始值上的属性/方法时,JS引擎都会创建适当类型的对象包装器(Numberfor 5Stringfor 'str'Booleanfor true),并在该对象包装器上解析属性访问/方法调用。例如,这就是发生的情况true.toString()

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

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

string将持有的字符串连接mystr,并obj.toString()number将持有的加3obj.valueOf()

现在放在一起

five = dis.call(5)

dis.call(5)行为就像(5).dis()如果5确实有方法dis。为了解决方法调用,创建了对象包装,并在其上解决了方法调用。此时,五个指向原始值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不再是一个对象。这些行中的每一行都会创建Number的新实例,以解决对.wtf属性的访问。实例是独立的,因此在一个实例上设置属性将在另一实例上不可见。该代码完全等效于以下代码:

(new Number(6)).wtf;
(new Number(6)).wtf = 'potato?';
(new Number(6)).wtf;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.