为什么在ES6的严格模式下,`"foo".bar = 42;`会抛出`TypeError`错误?

25
根据ES5.1规范,程序"use strict;" "foo".bar = 42;会创建一个String对象,在其上分配一个属性,然后将该对象丢弃,导致没有可观察的影响,包括任何异常。(可以在类似Opera 12的ES5兼容JS实现中尝试以确认无效果。)
在现代JS实现中,它会抛出一个TypeError - 请尝试:

"use strict"; "foo".bar = 42;

我非常确定新的行为是由ES6规范所要求的,但尽管我已经多次阅读相关部分,我仍然看不到指定抛出TypeError的地方。实际上,关键部分似乎没有改变:

6.2.3.2 PutValue (V, W)#
1. 如果V是null或undefined,返回undefined。 2. 如果Type(V)不是引用类型,抛出ReferenceError异常。 3. 让base为GetBase(V)的结果。 4. 如果IsUnresolvableReference(V)为true,则 - a. 如果IsStrictReference(V)为true,抛出ReferenceError异常。 - b. 创建全局对象的新绑定并将其添加到全局环境记录中。这个绑定的名称是GetReferencedName(V),它的初始值是undefined,并且它被设置为可写入和可配置。 - c. 返回。 5. 如果IsPropertyReference(V)为true,则 - a. 让base为ToObject(GetBase(V))的结果。 - b. 让succeeded为? base.[[Set]](GetReferencedName(V), W, GetThisValue(V))的结果。 - c. 如果succeeded是false并且IsStrictReference(V)为true,则抛出TypeError异常。 - d. 返回。
规范(ES6或更高版本)在哪里规定抛出TypeError?

我在附录C中也没有看到它。 - user2357112
我开始怀疑这根本不在规范中。 - user2357112
1
这里是它被添加到 V8 中的位置,像往常一样没有引用:https://codereview.chromium.org/408183002/ 现在正在查看其他内容。 - Ry-
我的期望是 ToObject 显式地返回一个不可扩展的 String,因此 [[Set]] 将返回 false 并且第6步将会抛出异常,但我实际上并没有看到字符串对象被定义为不可扩展。 - loganfsmyth
@Paulpro:“ToObject只是复制[[Extensible]]属性”-你是在指这里的实现吗?他们甚至没有执行ToObject,所以不会太令人困惑。 - Ry-
1
@Paulpro:创建一个字符串对象,使用Object.preventExtensions使其不可扩展,并在严格模式下尝试设置其属性会为我产生不同的错误消息,其中提到“对象不可扩展”。对于问题中的情况,似乎根本没有经过对象。 - user2357112
2个回答

16

我想它在这里:http://www.ecma-international.org/ecma-262/7.0/#sec-ordinaryset

9.1.9.1. OrdinarySet (O, P, V, Receiver)

[...]

4.b. 如果Receiver的类型不是Object,则返回false。

(在ES6 §9.1.9中,以前被称为[[Set]]。)

虽然PutValuebase提升为一个对象,但它并没有对接收器进行同样的处理——GetThisValue(V)仍然在原始的V(带有原始基数)上调用。所以,GetThisValue返回一个原始值,OrdinarySet.4b无法分配新创建的ownDesc并返回false,这反过来又导致PutValue.6d抛出TypeError(如果引用是严格的)。

V8的相应部分似乎遵循相同的逻辑:

Maybe<bool> Object::AddDataProperty(....
  if (!it->GetReceiver()->IsJSReceiver()) {
    return CannotCreateProperty(...

https://github.com/v8/v8/blob/3b39fc4dcdb6593013c497fc9e28a1d73dbcba03/src/objects.cc#L5140


不应该到达那里,因为 ownDesc 应该是未定义的。 - user2357112
那又怎样?第三步没有返回。 - georg
1
不错的发现,看起来对我来说是正确的。它将沿着原型向上走,并最终到达顶层,创建一个数据描述符,并尝试将其设置在接收器上,这仍然是原始值。 - loganfsmyth
1
是的,提及 6.d 会更好,尽管这已经包含在问题中了。 - loganfsmyth
2
很奇怪,这似乎没有在附录C中涵盖。附录C仅供参考,但仍然很奇怪。 - user2357112
显示剩余3条评论

5

@georg的答案似乎是正确的ES6+解释,但看起来这种行为并不新鲜。从ES5.1 PutValue可以看出:

  1. 否则,如果V是一个属性引用,则

    a. 如果HasPrimitiveBase(V)为false,则让put成为base的[[Put]]内部方法,否则让put成为下面定义的特殊[[Put]]内部方法

    b. 使用base作为其this值调用put内部方法,并传递GetReferencedName(V)作为属性名称,W作为值,IsStrictReference(V)作为Throw标志

并且在引用的[[Put]]中:

  1. 否则,这是一个在瞬态对象O上创建自有属性的请求

    a. 如果Throw为true,则抛出TypeError异常。

感觉我可能误读了什么......但是这句话“这是在暂态对象O上创建自己属性的请求”指的还能是什么呢?


1
这是一个很好的观察。我不太确定在检查ES5.1规范时怎么会忽略了这一点,但看起来Opera 12开发人员也是如此。 - cpcallen

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