通过良好的旧ES5构造函数创建新对象时:新对象何时创建?
一个猜测:是在JS引擎遇到new
关键字后立即在构造函数执行之前立即创建的吗?
与上述类似,但对于类:什么时候创建新对象?
一个猜测:由于我们可以使用class
语法对内置对象进行子类化,因此我认为引擎必须知道哪种类型(exotic
与ordinary
)其父对象是。因此,我在想,也许当引擎遇到extends
关键字并且可以读取父对象是什么类型时,才创建新对象。
在两种情况下,何时设置原型属性?是在执行构造函数/ ClassBody之前还是之后?
注释1 :如果答案中可以包含指向两个创建中的每个ECMAScript specification的位置的链接,那就太好了。我一直在搜索很多东西,却找不到正确的算法步骤。
注释2 :“创建”是指在内存和类型集(异国与普通)中分配的空间最少。
答案 0 :(得分:2)
new
将调用Construct,而后者又将调用相关函数的内部[[Construct]]。我只会在这里讨论普通的[[Construct]],而不会在意具有自定义行为的代理,因为与该主题无关。
在标准情况下(否extends
),在步骤5.a中,[[Construct]]调用OrdinaryCreateFromConstructor,并且其返回值将用作this
(请参见OrdinaryCallBindThis,用作参数)。请注意,OrdinaryCallEvaluateBody在后面的步骤中出现-创建对象,然后评估构造函数。对于new f
,基本上是Object.create(f.prototype)
。通常,它是Object.create(newTarget.prototype)
。 class
和ES5方式相同。显然,原型也在那里设置。
混淆可能源自使用extends
的情况。在这种情况下,[[ConstructorKind]]不是“基础”(请参见ClassDefinitionEvaluation的步骤15),因此在[[Construct]]中,步骤5.a不再适用,也不会调用OrdinaryCallBindThis。这里的重要部分发生在super call中。长话短说,它使用SuperConstructor和当前的newTarget调用Construct,并将结果绑定为this
。因此,您可能知道,超级调用之前对this
的任何访问都会导致错误。这样,将在超级调用中创建“新对象”(请注意,所讨论的内容再次适用于对Construct的调用-如果SuperConstructor不扩展任何内容,则是非派生的情况,否则是这种情况-唯一的区别是newTarget )。
要详细说明newTarget转发,请参见以下示例:
class A { constructor() { console.log(`newTarget: ${new.target.name}`); } }
class B extends A { constructor(){ super(); } }
console.log(
`B.prototype's prototype: ${Object.getPrototypeOf(B.prototype).constructor.name}.prototype`
);
console.log("Performing `new A();`:");
new A();
console.log("Performing `new B();`:");
new B();
由于[[Construct]]始终以newTarget作为参数调用OrdinaryCreateFromConstructor,因此总是使用该原型,最后它是正确的原型(在上面的示例B.prototype
中,请注意,这反过来又A.prototype
作为原型,也称为Object.getPrototypeOf(B.prototype) === A.prototype
)。最好查看所有相关部分(超级调用,构造,[[Construct]]和OrdinaryCreateFromConstructor),并观察它们如何获得/设置或传递newTarget。还要注意,对PrepareForOrdinaryCall的调用也会获取newTarget,并将其设置在相关的SuperConstructor调用的FunctionEnvironment中,以便其他链接的超级调用也将获得正确的调用(对于从某对象扩展而又从该对象扩展而来的情况)东西)。
最后但最不重要的一点是,构造函数可以使用return
生成所需的任何对象。这通常导致在前面描述的步骤中创建的对象被简单地丢弃。但是,您可以执行以下操作:
const obj = {};
class T extends Number {
constructor() {
return obj;
}
}
let awkward = new T();
在这种非常尴尬的情况下,没有调用super
,但这也不是错误,因为构造函数只是返回了一些先前创建的对象。在这里,至少从我的理解来看,使用new T()
时根本不会创建任何对象。
还有另一个副作用。如果您从一个构造函数进行扩展,该构造函数返回一些自制的对象,newTarget的转发以及所有无效的函数,则扩展类的原型将丢失:
class A {
constructor() {
// The created object still has the function here.
// Note that in all normal cases, this should not
// be in the constructor of A, it's just to show
// what is happening.
this.someFunc();
//rip someFunc, welcome someNewFunc
return {
someNewFunc() { console.log("I'm new!"); }
};
}
}
class B extends A {
constructor() {
super();
//We get the new function here, after the call to super
this.someNewFunc();
}
someFunc() { console.log("something"); }
}
console.log("Performing `new B();`:");
let obj = new B();
console.log("Attempting to call `someFunc` on the created obj:");
obj.someFunc(); // This will throw an error.
PS:我自己也是第一次在规范中阅读很多内容,因此可能会有一些错误。我自己的兴趣是找出扩展内置组件是如何工作的(源于不久前的另一场辩论)。要理解的是,在上述之后,只需要最后一件事:我们注意到对于Number constructor,它会检查“是否未定义NewTarget [...]”,否则将使用NewTarget正确调用OrdinaryCreateFromConstructor,同时添加内部[[NumberValue]]插槽,然后在下一个位置进行设置步骤。
编辑以尝试回答评论中的问题:
我认为您仍然将class
和ES5方式看作是两个独立的东西。 class
几乎完全是语法糖,对此问题的评论中已经提到。一个类不过是一个函数,类似于“旧的ES5方法”。
关于第一个问题,您提到的“方法”是该函数,该函数将以ES5方式使用(以及变量将保存什么,class A extends Number {}; console.log(typeof A === "function" && Object.getPrototypeOf(A) === Number);
)。设置原型是为了实现您先前所说的“继承静态属性”。静态属性不过是构造函数上的属性(如果您曾经使用过ES5方式)。
[{HomeObject]]用于访问super
,如table 27中所述。如果您查看相关调用的功能(请参阅table 27,GetSuperBase),则实际上,您会注意到“ [[HomeObject]]。[[GetPrototypeOf]]()”的作用。那样就应该是超类原型,这样super.someProtoMethod
就可以在超类的原型上工作。
对于第二个问题,我认为最好仅举一个例子:
class A { constructor() { this.aProp = "aProp"; } }
class B extends A { constructor() { super(); this.bProp = "bProp"; }
new B();
在评估new B();
时,我将尝试列出有趣的步骤,依次执行:
new
调用Construct,由于没有当前的newTarget,因此调用B
的[[Construct]],并且newTarget现在设置为B
。
[[Construct]]遇到了一种不是“基础”的类型,因此不会创建任何对象
PrepareForOrdinaryCall,用于构造函数的执行,生成一个新的执行上下文以及一个新的FunctionEnvironment(其中[[NewTarget]]将设置为newTarget!),并使其成为正在运行的执行上下文。 / p>
OrdinaryCallBind这也不执行,并且this
保持未初始化
OrdinaryCallEvaluateBody现在将开始执行B
遇到并执行了超级调用:
GetNewTarget()从先前设置的FunctionEnvironment中检索[[NewTarget]]
Construct,并使用检索到的newTarget
它使用newTarget调用SuperConstructor的[[Construct]]
SuperConstructor具有种类“ base”,因此它执行OrdinaryCreateFromConstructor,但设置了newTarget。现在本质上是Object.create(B.prototype)
,并再次指出,Object.getPrototypeOf(B.prototype) === A.prototype
已经在类构造中的函数B
上进行了设置。
与上面类似,正在创建一个新的执行上下文,这一次,OrdinaryCallBindThis也已完成。 SuperConstructor将执行,产生一些对象,再次弹出执行上下文。请注意,应该A
再次扩展其他内容,则将newTarget再次正确地设置到任何地方,这样它就会变得越来越深。
super从Construct(从B.prototype
作为原型的SuperConstructor产生的对象中获取结果,如果没有特殊情况发生,如所讨论的,例如,构造方法返回其他值,或原型)已被手动更改),并在当前环境中将其设置为this
,这是用于执行B
的构造函数的(另一个已经弹出)。
B
的构造函数的,已初始化this
。它是一个对象,它以B.prototype
作为原型,而它又以A.prototype
作为原型,并且已经在其上调用A
构造函数(同样,如果没有例外,则不会发生),因此this.aProp
已经存在。然后B
的构造函数将添加bProp
,而该对象是new B();
的结果。
答案 1 :(得分:2)
通过良好的旧ES5构造函数创建新对象时:新对象何时创建?
对象构造行为的规范级定义由[[Construct]]
函数定义。对于标准JS函数(function Foo(){}
,此函数的定义在9.2.3 FunctionAllocate中初始化,其中functionKind
将"normal"
。然后您可以在步骤9.a
上看到[[Construct]]
插槽被声明为指向section 9.2.2,并且[[ConstructorKind]]
设置为"base"
。
当用户代码调用new Foo();
来构造此函数的实例时,它将从12.3.3 The new
operator到12.3.3.1.1 EvaluateNew到7.3.13 Construct到[[Construct]]
进行调用,这会调用上面初始化的插槽,传递参数,而Foo
的作用为newTarget
。
深入到9.2.2 [[Construct]]
,我们可以看到步骤5.a
执行以下操作:
- a。让
thisArgument
为?OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%")
。
回答您的何时问题。 this
对象实际上是通过执行Object.create(Foo.prototype)
(在其中带有一些额外的可忽略逻辑)来创建的。然后,该函数将继续执行,并在步骤8
中执行
- 如果种类为
"base"
,请执行OrdinaryCallBindThis(F, calleeContext, thisArgument)
。
您可能会想到做this = thisArgument
,它将在函数{上实际调用this
函数的逻辑之前设置函数中Foo
的值{1}}。
ES6类与ES5样式构造函数的主要区别在于11
方法在构造的 first 级别仅使用一次。例如,如果我们有
[[Construct]]
function Parent(){}
function Child(){
Base.apply(this, arguments);
}
Object.setPrototype(Child.prototype, Parent.prototype);
new Child();
将new
用于[[Construct]]
,而对Child
的调用则使用Parent
,这意味着它实际上不是在构造父对象,就像正常函数一样调用它,并传递适当的.apply
值。
正如您所注意到的,这是事情变得复杂的地方,因为这意味着this
实际上对Parent
的创建没有任何影响,而只是希望它是给出可接受的值。
与上述类似,但对于类:什么时候创建新对象?
ES6类语法的主要区别在于,因为用this
而不是super()
/ Parent.call
来调用父函数,所以调用了父函数的Parent.apply
函数而不是[[Construct]]
。因此,实际上可以将[[Call]]
设置为[[ConstructorKind]]
以外的其他内容来进入9.2.2 [[Construct]]
。行为的这种变化会影响对象的构造时间。
如果我们现在使用ES6类重温上面的示例
"base"
class Parent {
constructor() {
}
}
class Child extends Parent {
constructor() {
super();
}
}
不是Child
,因此当"base"
构造函数最初运行时,Child
值未初始化。您可以将this
视为super()
,就像
const this = super();
将引发异常,因为尚未初始化console.log(value);
const value = 4;
,因此对value
的调用会调用父super()
,然后在内部初始化[[Construct]]
this
构造函数主体的名称。如果Child
是[[Construct]]
,则父function Parent(){}
的行为就像在ES5中一样,因为它是[[ConstructorKind]]
。
此行为也是允许ES6类语法扩展本机类型(如"base"
)的原因。对Array
的调用实际上是创建实例的原因,并且由于super()
函数知道创建真实的函数数组所需的全部知识,因此可以做到这一点,然后返回该对象。 / p>
在两种情况下,何时设置原型属性?是在执行构造函数/ ClassBody之前还是之后?
我上面提到的另一个关键之处是规范摘要中上面提到的Array
的确切性质。在ES6中,有一个新概念,即“新目标”,它是传递给newTarget
的实际构造函数。因此,如果您使用new
,则实际上是以两种不同的方式使用new Foo
。一种是将函数用作构造函数,而另一种是将值用作“新目标”。这对于嵌套类构造函数至关重要,因为当您调用Foo
函数链时,实际调用的构造函数将在链上运行,但是[[Construct]]
的值将保持不变。这很重要,因为newTarget
是用来实际设置最终构造对象的原型的东西。例如,当您这样做
newTarget.prototype
对class Parent extends Array {
constructor() {
console.log(new.target); // Child
super();
}
}
class Child extends Parent {
constructor() {
console.log(new.target); // Child
super();
}
}
new Child();
的调用将调用new Child
构造函数,并将其设置为Child
的{{1}}值。然后,在调用newTarget
时,我们将使用Child
中的super()
,但仍将[[Construct]]
传递为Parent
的值。对于Child
重复此操作,这意味着即使newTarget
负责创建数组奇异对象,它仍可以使用Parent
(Array
)来确保该数组具有正确的原型链。