最近的一条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]]属性的数字,但是5 ++和5 * 5的结果似乎只是普通的数字5和25(不是数字) 为什么是五个。WTF属性在5++增量之后消失 为什么是五个。WTF属性在5++增量之后甚至不再是可设置的,尽管five。WTF =土豆?'赋值显然设置了值。


这很简单。

function dis () { return this; }

这将返回This上下文。因此,如果调用(5),则将数字作为对象传递。

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

返回的是object

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

但是当你执行++时,它会将对象转换为原始数字类型,从而删除.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”关键字如何工作?假设要调用的第一个参数被用作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()已定义。顺便说一句,这个没有返回,因为函数没有执行。

2.

    five = dis.call(5)

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

3.

   five.wtf = 'potato'

第一个返回“potato”,因为您刚刚将属性wtf(5)设置为“potato”。Javascript返回你所设置的变量的值,这样就可以很容易地将多个变量链接起来,并将它们设置为相同的值,如下所示:a = b = c = 2。

4.

    five * 5

这个返回25,因为你刚刚把原数5乘以了5。5的值由Number对象的值决定。

5.

    five.wtf

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

6.

    five++

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

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

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

or

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

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

我希望这能有所帮助,javascript喜欢假设事情,所以当从数字对象更改为原始数字时,它可能会令人困惑。 你可以使用typeof关键字来检查某个东西是什么类型 typeof五 初始化后,它返回'object',执行5++后,它返回'number'。

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


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

So:

> 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

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

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

在五个变量上声明wtf属性

five.wtf = 'potato'
"potato"

5 .wtf的值

five.wtf
"potato"

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

five * 5
25

5 .wtf的值

five.wtf
"potato"

在这里打开5。5现在只是一个原始数。它输出5,然后5加1。

five++
5

5现在是原数6,它没有任何属性。

five.wtf
undefined

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

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

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

five.wtf
undefined

5是6,因为上面的增量

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声明了一个返回上下文对象的函数dis。它所代表的内容取决于您是否使用严格模式。如果函数声明为:

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

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

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

02是函数声明的返回值。Undefined在这里应该是不言自明的。

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

返回值为奇数Number {[[PrimitiveValue]]: 5},表示封装了原语值5的对象

5对象的WTF属性被赋值为'potato'

06是赋值的返回值,应该是不言自明的。

07五个对象的WTF属性正在检查中

八是五。WTF之前设置为“土豆”,它在这里返回“土豆”

5对象乘以原始值5。这与其他被相乘的对象没有什么不同,ES5规范的11.5节对此进行了解释。特别值得注意的是如何将对象转换为数值,这将在几节中介绍。

9.3,当时:

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

9.1 ToPrimitive:

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

8月12日8 [DefaultValue]:

让valueOf是调用对象O的[[Get]]内部方法的结果,参数为valueOf。 如果IsCallable(valueOf)为真,则, 让val是调用valueOf的[[Call]]内部方法的结果,O作为this值和一个空参数列表。 如果val是一个基本值,则返回val。

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

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

10 as fives valueOf函数没有改变,它返回被包装的原语值5,因此5 * 5的结果为5 * 5,结果为25

11 5个对象的WTF属性被再次计算,尽管它从05分配时就没有改变。

12“土豆”

在5处调用后缀增量操作符,它获取数值(5,我们前面介绍过),存储值以便返回,在值(6)上加上1,将值赋给5,然后返回存储值(5)

14和前面一样,返回值是它被加一之前的值

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

16号。Prototype没有WTF属性,因此返回undefined

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

18“土豆吗?'由赋值操作符返回

19再次访问值为6的5,并再次访问Number。Prototype没有WTF属性

20未定义如上所述

21五被访问

22返回6,如13所述


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

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

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

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

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

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

string将保存mystr和obj.toString()的字符串连接,number将保存3和obj.valueOf()的加法。

现在把它们放在一起

five = dis.call(5)

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

five.wtf = 'potato'

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

five * 5

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

five++

这实际上是5 = 5 . valueof() + 1。在这一行之前,第5行保存了围绕值5的对象包装器,而在这一行之后,第5行保存了基本值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上看到这个很有趣:)

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

Number value and Number object (a = 3 vs a = new Number(3)) are very different. One is a primitive, the other is an object. You cannot assign attributes to primitives, but you can to objects. Coercion between the two is implicit. For example: (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 Every Expression has a return value. When the REPL reads and executes an expression, this is what it displays. The return values often don't mean what you think and imply things that just aren't true.

好了,开始吧。

承诺。

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

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

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

现在我们设置属性5。wtf转换为'potato',并以5作为对象,它当然接受简单赋值。

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

以5为对象,我确保它仍然可以执行简单的算术运算。它可以。它的属性仍然存在吗?是的。

把。

> five++
5
> five.wtf
undefined

现在我们检查5++。后缀增量的技巧是,整个表达式将根据原始值求值,然后再增加该值。看起来5还是5,但实际上表达式的值是5,然后把5设为6。

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

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

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

信誉。

> five
6

从后缀的增量开始,5就变成了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是对的。数字不能有自定义属性——我们都知道!这是他们在警校教你的第一件事。

我们应该在看到控制台输出的那一刻就知道——这不是我们认为的数字。这是一个冒名顶替者——一个把自己冒充成我们可爱的无辜数字的物体。

这是……(5)新号码。

当然!这完全说得通。Call()有工作要做,他必须调用一个函数,要做到这一点,他需要填充这个,他知道他不能用一个数字来做——他需要一个对象,他愿意做任何事情来得到它,即使这意味着强制我们的数字。当call()看到数字5时,他看到了一个机会。

这是一个完美的计划:等到没人注意的时候,把我们的号码换成一个看起来和它一模一样的东西。我们得到一个数字,函数被调用,没有人会知道。

这真是个完美的计划,但就像所有的计划一样,即使是完美的计划,上面有个洞,我们马上就要掉进去了。

看,call()不明白的是,他不是镇上唯一一个可以胁迫数字的人。这毕竟是JavaScript——强制无处不在。

call()拿走了我的号码,直到我把他的小冒名顶替者的面具拉下来,并将他暴露给整个Stack Overflow社区,我才会停止。

但如何?我需要一个计划。当然它看起来像一个数字,但我知道它不是,一定有办法证明这一点。就是这样!它看起来像一个数字,但它能像一个数字吗?

我告诉五个人我需要他变大五倍——他没有问为什么,我也没有解释。然后我做了任何优秀程序员都会做的事情:相乘。当然,他不可能通过造假来摆脱这一切。

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

该死的!不仅5乘起来很好wtf还在那里。该死的家伙和他的土豆。

到底发生了什么事?我错了吗?五真的是个数字吗?不,我一定遗漏了什么,我知道,我一定忘记了什么,一些如此简单和基本的东西,以至于我完全忽略了它。

这看起来不太好,我已经写了几个小时的答案,但我仍然没有更接近我的观点。我不能再坚持下去了,最终人们会停止阅读,我必须想出点什么,而且必须要快。

等等,就是这个!5不是25,25是结果,25是一个完全不同的数字。当然,我怎么会忘记呢?数字是不可变的。当你用5乘5的时候,什么都没有分配给任何东西你只是创建了一个新的数字25。

这肯定就是这里发生的事情。当我用5乘5的时候,5一定会变成一个数字这个数字一定是用来做乘法的。输出到控制台的是乘法运算的结果,而不是5本身的值。5没有分配到任何东西,所以它当然不会改变。

那么我如何让5把操作的结果赋给自己呢?我明白了。五点还没来得及思考,我就大喊“++”。

> five++
5

啊哈!我抓到他了!每个人都知道5 + 1 = 6,这就是我需要证明5不是数字的证据!那是个骗子!一个不会数数的骗子。我可以证明这一点。实数是这样的:

> num = 5
5
> num++
5

等待?这里发生了什么?唉,我太忙了,都忘了后期操作员是怎么工作的了。当我在5的末尾使用++时,我说的是返回当前值,然后增加5。它是将操作发生之前的值打印到控制台。num实际上是6,我可以证明它:

>num
6

是时候看看5到底是什么了:

>five
6

...这正是它应该有的样子。5个不错,但我比他更好。如果5仍然是一个对象,这意味着它仍然具有wtf属性,我愿意赌它没有的一切。

> five.wtf
undefined

啊哈!我是对的。我抓到他了!五现在是一个数字,不再是一个物体了。我知道乘法技巧这次救不了我。5 ++实际上是5 = 5 + 1。与乘法运算不同,++运算符将值赋给5。更具体地说,它将5 + 1的结果赋给它就像在乘法的情况下返回一个新的不可变数。

我知道他已经在我手上了,为了确保他逃不掉。我还有一个测试要做。如果我是对的,5现在真的是一个数字,那么这就行不通了:

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

这次他不会再骗我了。我知道土豆?会被打印到控制台因为那是赋值的输出。真正的问题是,wtf还会在那里吗?

> five.wtf
undefined

正如我怀疑的那样——什么都没有——因为数字不能被赋予属性。我们在学院的第一年就知道了;)

谢谢内森。多亏你有勇气问我这个问题,我终于可以把这一切抛在脑后,开始一个新的案子。

就像这个关于toValue()函数的例子。哦,天哪。人参公鸡!


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

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

Function.prototype.call() (from MDN)

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

一旦很明显5被转换为新的数字(5),其余的应该是相当明显的。注意,其他示例也可以工作,只要它们是基本值。

函数primitiveToObject(的){ 返回dis.call(的); } 函数dis(){返回this;} / /现有的例子 console.log (primitiveToObject (5)); / /∞ console.log (primitiveToObject (1/0)); / / bool console.log (primitiveToObject (1 > 0)); / /字符串 console.log (primitiveToObject(“hello world”); <img src="http://i.stack.imgur.com/MUyRV.png" />