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

最近的一条 tweet 包含了这段 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]]属性的 Number,而 five++five * 5的结果似乎只是简单的数字 525(而不是 Numbers)
  • 为什么 five.wtf属性在 five++增量之后消失
  • 为什么 five.wtf属性在 five++递增之后不再可以设置,尽管 five.wtf = 'potato?'赋值明显设置了值。
15751 次浏览

这很简单。

function dis () { return this; }

返回this上下文。所以,如果你执行call(5),你是将数字作为对象传递。

call函数不提供参数,你给出的第一个参数是this的上下文。通常如果你想要它在上下文上,你给它{},所以dis.call({}),这意味着函数中的this是一个空的this。然而,如果你传递5,它似乎会被转换为一个对象。看到打电话给

所以返回值是object

当你执行five * 5时,JavaScript将对象five视为基本类型,因此等同于5 * 5。有趣的是,执行'5' * 5,它仍然等于25,因此JavaScript显然是在底层进行强制转换。在这一行中没有对底层的five类型进行任何更改

但是当你执行++时,它会将对象转换为基本类型number,从而删除.wtf属性。因为您正在影响底层类型

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

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的第一个参数被用作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

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

1.

    function dis() { return this }

创建函数dis(),但因为它没有被设置为var,所以没有返回值,所以undefined是输出,即使定义了dis()。顺便说一句,this没有返回,因为函数没有执行。

2.

    five = dis.call(5)

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

3.

   five.wtf = 'potato'

第一个返回"potato",因为你刚刚将five的属性wtf设置为'potato'。Javascript返回你所设置的变量的值,这使得链接多个变量并将它们设置为相同的值变得很容易,就像:a = b = c = 2

4.

    five * 5

这将返回25,因为您刚刚将原语数5乘到fivefive的值由Number对象的值决定。

5.

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

6.

    five++

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

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

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

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

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

我希望这有帮助,javascript喜欢假设事物,所以当从Number对象更改为基本的number时,它可能会令人困惑。 你可以通过使用typeof关键字来检查类型,写入 typeof五 在你初始化后,它返回'object',在你执行five++后,它返回'number'

@ desze非常好地描述了Number对象和基本数之间的区别。

原始值不能有属性。但是,当您试图访问原始值上的属性时,它会透明地转换为临时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调用dis。原语值在严格模式下作为上下文传递时被装箱(中数)。所以five现在是object(盒装数字)。

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

five变量上声明wtf属性

five.wtf = 'potato'
"potato"

five.wtf

five.wtf
"potato"

five被装箱5,所以它同时是数字和对象(5 * 5 = 25)。它不会改变five

five * 5
25

five.wtf

five.wtf
"potato"

在这里打开fivefive现在只是原始的number。它输出5,然后将1添加到five

five++
5

five现在是基本数6,其中没有任何属性。

five.wtf
undefined

基本类型不能有属性,你不能设置这个

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

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

five.wtf
undefined

five6,因为上面是后增量

five
6
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应该不言自明。

当在原语值5的上下文中调用变量five时,变量five被初始化为返回值dis。因为dis不是严格模式,所以这一行等同于调用five = Object(5)

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

05 five对象的wtf属性被赋值为'potato'的字符串值

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

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

08作为five.wtf之前设置为'potato',它在这里返回'potato'

09five对象乘以原语值5。这与任何其他被相乘的对象没有区别,并在ES5规范的第11.5节中解释。特别值得注意的是如何将对象转换为数值,这将在几节中介绍。

9.3,当时:

  1. 让primValue为ToPrimitive(输入参数,提示数字)。
  2. 返回,当时(primValue)。

9.1 ToPrimitive:

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

8.12.8 [[DefaultValue]]:

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

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

    1. 让val是调用valueOf的[[Call]]内部方法的结果,O作为this值和一个空参数列表。
    2. 如果val是一个基本值,则返回val。
    3. 李< / ol > < / >

这完全是一种迂回的方式,表示对象的valueOf函数被调用,该函数的返回值被用于方程中。如果你要改变valueOf函数,你可以改变操作的结果:

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

10作为__abc1的valueOf函数没有改变,它返回包装的原语值5,因此five * 5计算为5 * 5,结果为25

11: five对象的wtf属性被求值为再一次,尽管它在05上被赋值时没有改变。

12 'potato'

13 后缀增量运算符five上被调用,它获取数值(5,我们前面介绍过),存储值以便返回,将1添加到值(6),将值赋值给five,并返回存储值(5)

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

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

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

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

18 'potato?'由赋值操作符返回

19再次访问值为6five,并且Number.prototype同样没有wtf属性

如上所述,20 undefined

21 five被访问

22 6返回,如13所述

有几个概念可以解释发生了什么

5是一个数字,一个基本值

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

每当你访问一个原语值的属性/方法时,JS引擎将创建一个适当类型的对象包装器(Number用于5String用于'str'Boolean用于true),并解析该对象包装器上的属性访问/方法调用。例如,当你执行true.toString()时,就会发生这种情况。

当对对象执行操作时,它们被转换为基本值(通过使用toStringvalueOf),以解析这些操作-例如在做

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

string将保存mystrobj.toString()的字符串拼接,而number将保存3obj.valueOf()的加法。

现在把它们放在一起

five = dis.call(5)

如果5实际上有方法dis,那么dis.call(5)的行为就像(5).dis()一样。为了解析方法调用,将创建对象包装器,并在其上解析方法调用。此时,five指向围绕原语值5的对象包装器。

five.wtf = 'potato'

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

five * 5

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

five++

这实际上是five = five.valueOf() + 1在这一行之前,__ABC1保存了值5周围的对象包装器,而在这一行之后,__ABC1保存了原语值6

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

five不再是一个对象。每一行都创建一个新的Number实例,以便解析.wtf属性访问。实例是独立的,因此在一个实例上设置属性在另一个实例上是不可见的。代码完全等价于下面的代码:

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

OP。在Stack Overflow上看到这个很有趣:)

在详细介绍这种行为之前,有几件事是很重要的:

  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. 每个表达式都有一个返回值。当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,然后将five设置为6

不仅five被设置为6,而且它被强制返回为Number值,并且所有属性都丢失了。由于原语不能保存属性,所以five.wtf是未定义的。

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

我再次尝试将属性wtf重新分配给five。返回值表明它是固定的,但实际上不是,因为five是一个Number值,而不是一个Number对象。表达式的计算结果为'potato?',但当我们检查时,我们看到它没有被赋值。

信誉。

> five
6

从后缀增量开始,five就变成了6

在JavaScript世界里有强制——一个侦探故事

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

我已经调查了好几周了。这一切都始于去年十月的一个暴风雨之夜。我无意中发现了Number类——我的意思是,为什么JavaScript会有Number类呢?

我对接下来要发现的事情毫无准备。

事实证明,JavaScript在不告诉你的情况下,已经在你眼皮底下把你的数字变成对象,把对象变成数字。

JavaScript希望没有人会发现,但人们已经报告了奇怪的意想不到的行为,现在多亏了你和你的问题,我有了我需要的证据来揭露这件事。

这是我们目前所发现的。我不知道我是否应该告诉你这个——你可能想要关闭你的JavaScript。

> function dis() { return this }
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 * 5时,five必须被强制转换成一个数字,而这个数字必须是用于乘法的数字。打印到控制台的是乘法运算的结果,而不是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()函数的。哦,天哪。人参公鸡!

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

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

Function.prototype.call() (from MDN)

< p > 语法
fun.call(thisArg[, arg1[, arg2[, ...]]]) < / p > < p > thisArg
这一价值提供了对乐趣的召唤。注意,这可能不是方法所看到的实际值:如果方法是一个非严格模式代码中的函数,null和undefined将被全局对象和原始值将被转换为对象替换。(强调我的)< / p >

一旦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" />

enter image description here