简体   繁体   English

什么时候在JavaScript的构造函数和类中创建新对象?

[英]When are new objects created in JavaScript's constructor function vs in class?

Constructor Function 构造函数

When creating a new object via a good old ES5 constructor function: When is the new object created? 通过良好的旧ES5构造函数创建新对象时:新对象何时创建?

A guess : Is it created immediately when the JS engine encounters the new keyword, directly before the constructor function is executed? 猜想 :是在JS引擎遇到new关键字时立即在构造函数执行之前立即创建的吗?


Class

Similarly to above, but for classes: When is the new object created? 与上述类似,但对于类:新对象何时创建?

A guess : Since we can subclass built-in objects with class syntax, I am thinking the engine must know what type ( exotic vs ordinary ) its parent object is. 一个猜测 :由于我们可以使用class语法对内置对象进行子class ,因此我认为引擎必须知道其父对象是什么类型( exotic ordinary )。 Therefore, I was thinking perhaps the new object is created right when the engine encounters the extends keyword and can read what type the parent is. 因此,我在想,也许当引擎遇到extends关键字并可以读取父对象的类型时,就可以创建新对象。


Lastly 最后

In both cases, when is the prototype property set? 在这两种情况下,原型属性何时设置? Is it before or after executing the constructor function / ClassBody? 是在执行构造函数/ ClassBody之前还是之后?


Notes 笔记

Note 1 : It would be great if the answer could include links to where in the ECMAScript specification each of the two creations occur. 注1 :如果答案可以包括两个创建中的每一个在ECMAScript规范中的位置的链接,那将是很好的。 I have been searching around a lot and have been unable to find the right algorithm-steps. 我一直在搜索很多东西,却找不到正确的算法步骤。

Note 2 : With "created" I mean space allocated in memory and type set (exotic vs ordinary), at a minimum. 注2 :“创建”是指最少分配给内存和类型集的空间(异物对普通)。

new will call Construct , which in turn will call the related function's internal [[Construct]] . new将调用Construct ,然后依次调用相关函数的内部[[Construct]] I will only discuss the normal [[Construct]] here, and not care about eg Proxies having custom behavior for it, as that is imho not related to the topic. 我将仅在此处讨论常规的[[Construct]],而不关心例如具有自定义行为的代理,因为这与该主题无关。


In the standard scenario (no extends ), in step 5.a, [[Construct]] calls OrdinaryCreateFromConstructor , and the return of that will be used as this (see OrdinaryCallBindThis , where it is used as argument). 在标准情况下(no extends ),在步骤5.a中,[[Construct]]调用OrdinaryCreateFromConstructor ,并将其返回值用作this (请参阅OrdinaryCallBindThis ,将其用作参数)。 Note that OrdinaryCallEvaluateBody comes at a later step - the object is created, before the constructor function is evaluated. 请注意,OrdinaryCallEvaluateBody在后面的步骤中出现-创建对象,然后评估构造函数。 For new f , it is basically Object.create(f.prototype) . 对于new f ,基本上是Object.create(f.prototype) Generally, it's Object.create(newTarget.prototype) . 通常,它是Object.create(newTarget.prototype) This is the same for class and the ES5 way. 这与class和ES5方式相同。 The prototype is obviously set there aswell. 原型显然也设置在那里。


The confusion probably stems from the case, where extends is being used. 混乱可能源于正在使用extends的情况。 In that case, [[ConstructorKind]] is not "base" (see step 15 of ClassDefinitionEvaluation ), so in [[Construct]], step 5.a does not apply anymore, nor is OrdinaryCallBindThis called. 在那种情况下,[[ConstructorKind]]不是“基础”(请参阅ClassDefinitionEvaluation的步骤15),因此在[[Construct]]中,步骤5.a不再适用,也不会调用OrdinaryCallBindThis。 The important part here happens in the super call . 这里的重要部分发生在超级调用中 Long story short, it calls Construct with the SuperConstructor and current newTarget, and binds the result as this . 长话短说,它使用SuperConstructor和当前的newTarget调用Construct,并将结果绑定为this Accordingly, as you may know, any access to this before the super call results in an error. 因此,您可能知道,在超级调用之前this任何访问都将导致错误。 As such, the "new object" is created in the super call (note that the discussed applies again to that call to Construct - should the SuperConstructor not extend anything, the non-deriving case, otherwise this one - with the only difference being newTarget). 这样,将在超级调用中创建“新对象”(请注意,所讨论的内容再次适用于对Construct的调用-如果SuperConstructor不扩展任何内容,则是非派生的情况,否则是这种情况-唯一的区别是newTarget )。

To elaborate on the newTarget forwarding, here is an example of how this behaves: 为了详细说明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(); 

As [[Construct]] calls OrdinaryCreateFromConstructor with newTarget as parameter, which is always forwarded, the prototype used will be the correct one at the end (in above example, B.prototype , and note that this in turn has A.prototype as prototype, aka Object.getPrototypeOf(B.prototype) === A.prototype ). 由于[[Construct]]始终以newTarget作为参数调用OrdinaryCreateFromConstructor,因此总是使用正确的原型(在上面的示例中为B.prototype ,请注意,该原型又将A.prototype作为原型)。 ,又名Object.getPrototypeOf(B.prototype) === A.prototype )。 It's good to look at all the related parts (super call, Construct, [[Construct]], and OrdinaryCreateFromConstructor), and watch how they get/set or pass newTarget along. 最好查看所有相关部分(超级调用,构造,[[Construct]]和OrdinaryCreateFromConstructor),并观察它们如何获得/设置或传递newTarget。 Note here aswell that the call to PrepareForOrdinaryCall also gets the newTarget, and sets it in the FunctionEnvironment of related SuperConstructor calls, so that additional chained super calls will obtain the correct one aswell (for the case of extending from something that is in turn extending from something). 还要注意,对PrepareForOrdinaryCall的调用也会获取newTarget,并将其设置在相关的SuperConstructor调用的FunctionEnvironment中,以便其他链接的超级调用也将获得正确的调用(对于从某对象扩展而又从该对象扩展而来的情况)东西)。


Last but least, constructors can use return to produce any object they want. 最后但最不重要的一点是,构造函数可以使用return来生成他们想要的任何对象。 This usually leads to the objects created in the previously described steps to be simply discarded. 这通常导致在前面描述的步骤中创建的对象被简单地丢弃。 However, you can do the following: 但是,您可以执行以下操作:

const obj = {};
class T extends Number {
  constructor() {
    return obj;
  }
}
let awkward = new T();

In this very awkward case, there is no call to super , which is however also no error, as the constructor simply returns some previously made object. 在这种非常尴尬的情况下,没有调用super ,但是也没有错误,因为构造函数只是返回了一些先前创建的对象。 Here, at least from what i could see, no object will be created at all when using new T() . 在这里,至少从我的观察中可以看出,使用new T()时完全不会创建任何对象。

There is another side effect. 还有另一个副作用。 Should you extend from a constructor, which returns some self-made object, the forwarding of newTarget and all that has no effect, the prototype of the extending class is simply lost: 如果您从构造函数进行扩展,该构造函数返回一些自制的对象,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: I read a lot of this in the spec for the first time myself aswell, so there may be some mistakes. PS:我自己也是第一次在规范中阅读很多内容,因此可能会有一些错误。 My own interest was to find out how extending built-ins works (stemming from a different debate from a while ago). 我自己的兴趣是找出扩展内置组件是如何工作的(源于不久前的另一场辩论)。 To understand that, after the above, needs only one last thing: we notice eg for the Number constructor , that it checks for "If NewTarget is undefined [...]", and otherwise properly calls OrdinaryCreateFromConstructor, with NewTarget, while adding the internal [[NumberValue]] slot, then setting it in the next step. 要了解,在完成上述操作之后,只需要最后一件事:例如,对于Number构造函数 ,它会检查“ NewTarget是否未定义[…]”,否则在添加新目标的同时正确地调用带有NewTarget的OrdinaryCreateFromConstructor。内部[[NumberValue]]插槽,然后在下一步中进行设置。


Edit to attempt answering questions in the comments: 编辑以尝试回答评论中的问题:

I think you are still looking at class and the ES5 way as two separate things. 我认为您仍将class和ES5方式视为两个独立的东西。 class is almost entirely syntactic sugar, as has already been mentioned in comments on the question. 正如对该问题的评论中提到的那样, class几乎完全是语法糖。 A class is nothing more than a function, similar to the "old ES5 way". 类只不过是一个函数,类似于“旧的ES5方法”。


Towards your first question, the "method" you mention, is the function, which one would use in the ES5 way (and what the variable will hold, class A extends Number {}; console.log(typeof A === "function" && Object.getPrototypeOf(A) === Number); ). 对于第一个问题,您提到的“方法”是函数,该函数将以ES5方式使用(以及变量将保存什么, class A extends Number {}; console.log(typeof A === "function" && Object.getPrototypeOf(A) === Number); )。 The prototype is set, to achieve what you earlier noted as "inheriting static properties". 设置原型是为了实现您先前所说的“继承静态属性”。 Static properties are nothing more than properties on the constructor (if you ever used the ES5 way). 静态属性不过是构造函数上的属性(如果您曾经使用过ES5方式)。

The [[HomeObject]] is used for access to super , as explained in table 27 . 表27中所述,[[HomeObject]]用于访问super If you look at what the related calls do (see table 27 , GetSuperBase ), you will notice it, in essence, just does "[[HomeObject]].[[GetPrototypeOf]]()". 如果查看相关调用的功能(请参见表27GetSuperBase ),则实际上,您会注意到它只是“ [[HomeObject]]。[[GetPrototypeOf]]()”。 That will be the superclass prototype, as it should be, so that super.someProtoMethod works on the superClass' prototype. 那将是应该的超类原型,以便super.someProtoMethod可在超类的原型上工作。


For the second question, i think it's best to just go through an example: 对于第二个问题,我认为最好仅举一个例子:

class A { constructor() { this.aProp = "aProp"; } }
class B extends A { constructor() { super(); this.bProp = "bProp"; }
new B();

I'll try to list the interesting steps, performed in order, when new B(); 我将尝试列出new B();时依次执行的有趣步骤new B(); is being evaluated: 正在评估中:

  • new calls Construct, which, as there is no current newTarget, calls [[Construct]] of B with newTarget now set to B . new调用Construct,因为没有当前的newTarget,所以调用B [[Construct]]并将newTarget设置为B

  • [[Construct]] encounters a kind which is not "base", and as such does not create any object [[Construct]]遇到了一种不是“基础”的类型,因此不会创建任何对象

  • PrepareForOrdinaryCall, for the execution of the constructor, generates a new execution context, along with a new FunctionEnvironment (where [[NewTarget]] will be set to newTarget!), and makes it the running execution context. PrepareForOrdinaryCall用于构造函数的执行,它会生成一个新的执行上下文以及一个新的FunctionEnvironment(其中[[NewTarget]]将设置为newTarget!),并使其成为正在运行的执行上下文。

  • OrdinaryCallBindThis is also not performed, and this stays uninitialized OrdinaryCallBindThis也没有执行, this保持未初始化

  • OrdinaryCallEvaluateBody will now start executing the constructor of B OrdinaryCallEvaluateBody现在将开始执行B的构造函数

  • The super call is encountered and executed: 遇到并执行超级调用:

    • GetNewTarget() retrieves the [[NewTarget]] from the FunctionEnvironment, which was previously set GetNewTarget()从先前设置的FunctionEnvironment中检索[[NewTarget]]

    • Construct is called on the SuperConstructor, with the retrieved newTarget 使用检索到的newTarget在SuperConstructor上调用Construct

    • It calls [[Construct]] of the SuperConstructor, with the newTarget 它使用newTarget调用SuperConstructor的[[Construct]]。

    • The SuperConstructor has kind "base", as such it performs OrdinaryCreateFromConstructor, but with the newTarget set. SuperConstructor具有种类“ base”,因此它执行OrdinaryCreateFromConstructor,但具有newTarget设置。 This is now in essence Object.create(B.prototype) , and note again, that Object.getPrototypeOf(B.prototype) === A.prototype , that's already set on the function B , from the class construction. 现在本质上是Object.create(B.prototype) ,再次注意, Object.getPrototypeOf(B.prototype) === A.prototype ,已经在类B的函数B上进行了设置。

    • Similarly to above, a new execution context is being made, and this time, OrdinaryCallBindThis is also done. 与上面类似,正在创建一个新的执行上下文,这一次,OrdinaryCallBindThis也已完成。 The SuperConstructor will execute, produce some object, the execution context is popped again. SuperConstructor将执行,产生一些对象,再次弹出执行上下文。 Note that should A in turn extend something else again, newTarget is properly set everywhere again, so it would just go deeper and deeper. 请注意,如果A反过来又扩展了其他内容,则newTarget会再次在各处正确设置,因此它会变得越来越深。

    • super takes the result from Construct (the object that the SuperConstructor produced, which does have B.prototype as prototype, should nothing exceptional happen - as discussed, eg the constructor returns some other value, or the prototype was manually changed), and sets it as this in the current environment, which is the one that is used to execute the constructor of B (the other has been popped already). super从Construct(SuperConstructor产生的对象,它确实具有B.prototype作为原型,应该在没有什么例外情况发生的情况下获取结果,如所讨论的,例如,构造函数返回其他值,或者手动更改了原型),并对其进行设置如this在当前环境下,其是用于执行的构造函数的一个B (另一个已被弹出)。

  • execution of the constructor of B continues, with this now initialized. 的构造函数的执行B仍在继续, this现在初始化。 It is an Object, which has B.prototype as prototype, which in turn has A.prototype as prototype, and on which the A constructor was already called (again, should nothing exceptional have happened), so this.aProp already exists. 它是一个对象,它具有B.prototype作为原型,又具有A.prototype作为原型,并且已经在其上调用了A构造函数(同样,应该没有发生任何异常),因此this.aProp已经存在。 The constructor of B will then add bProp , and that object is the result of new B(); 然后, B的构造函数将添加bProp ,并且该对象是new B();的结果new B(); .

When creating a new object via a good old ES5 constructor function: When is the new object created? 通过良好的旧ES5构造函数创建新对象时:新对象何时创建?

The spec-level definition of object construction behavior is defined by the [[Construct]] function. 对象构造行为的规范级定义由[[Construct]]函数定义。 For standard JS functions ( function Foo(){} , the definition of this function is initialized in 9.2.3 FunctionAllocate where functionKind will "normal" . Then you can see on step 9.a , the [[Construct]] slot is declared to point at section 9.2.2 and [[ConstructorKind]] is set to "base" . 对于标准JS函数( function Foo(){} ,此函数的定义在9.2.3 FunctionAllocate中初始化,其中functionKind"normal" 。然后您可以在步骤9.a看到[[Construct]]插槽已声明指向9.2.2节,并且[[ConstructorKind]]设置为"base"

When user code calls new Foo(); 当用户代码调用new Foo(); to construct an instance of this function, it will call from 12.3.3 The new operator to 12.3.3.1.1 EvaluateNew to 7.3.13 Construct to [[Construct]] , which calls the slot initialized above, passing the arguments, and the Foo function as newTarget . 构造此函数的实例,它将从12.3.3的new运算符调用12.3.3.1.1的EvaluateNew7.3.13的构造函数 [[Construct]] ,该调用上面初始化的插槽,并传递参数,并Foo函数作为newTarget

Digging into 9.2.2 [[Construct]] , we can see that step 5.a performs: 深入研究9.2.2 [[Construct]] ,我们可以看到步骤5.a执行:

  1. a. 一种。 Let thisArgument be ? thisArgument成为? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%") . OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%")

which answers your question of when . 回答您有关何时的问题。 The this object is created here by essentially doing Object.create(Foo.prototype) (with a little extra ignorable logic in there). this对象实际上是通过执行Object.create(Foo.prototype) (在其中带有一些额外的可忽略逻辑)来创建的。 The function will then continue along and at step 8 it will do 然后该功能将继续,并在步骤8执行

  1. If kind is "base" , perform OrdinaryCallBindThis(F, calleeContext, thisArgument) . 如果kind是"base" ,则执行OrdinaryCallBindThis(F, calleeContext, thisArgument)

which you can kind of think of as doing this = thisArgument , which will set the value of this in the function, before it actually calls the logic of the Foo function on step 11 . 您可以将其视为this = thisArgument ,它将在函数中实际在步骤11调用Foo函数的逻辑之前设置this的值。

The primary difference for ES6 classes vs ES5-style constructor functions is that the [[Construct]] methods are only used once, at the first level of construction. ES6类与ES5样式的构造函数的主要区别在于, [[Construct]]方法在构造的第一层仅使用一次。 For example, if we have 例如,如果我们有

function Parent(){}
function Child(){
  Base.apply(this, arguments);
}
Object.setPrototype(Child.prototype, Parent.prototype);

new Child();

the new will use [[Construct]] for Child , but the call to Parent uses .apply , meaning that it isn't actually constructing the parent, it's just calling it like a normal function and passing along an appropriate this value. new将为Child使用[[Construct]] ,但对Parent的调用使用.apply ,这意味着它实际上并不是在构造父对象,它只是像正常函数一样调用它并传递适当的this值。

This is where things become complicated, as you've noticed, because it means that Parent doesn't actually have any influence over the creation of this , and just has to hope that it is given an acceptable value. 正如您所注意到的,这是事情变得复杂的地方,因为这意味着Parent实际上对this的创建没有任何影响,而只是希望它被赋予可接受的价值。

Similarly to above, but for classes: When is the new object created? 与上述类似,但对于类:新对象何时创建?

The main difference with ES6 class syntax is that because the parent function is called with super() instead of Parent.call / Parent.apply , the [[Construct]] function of parent functions is called rather than [[Call]] . ES6类语法的主要区别在于,因为使用super()而不是Parent.call / Parent.apply来调用父函数,所以将调用父函数的[[Construct]]函数,而不是[[Call]] Because of this, it's actually possible to get into 9.2.2 [[Construct]] with [[ConstructorKind]] set to something other than "base" . 因此,实际上可以通过将[[ConstructorKind]]设置为"base"以外的其他内容来进入9.2.2 [[Construct]] It's this change in behavior that affects when the object is constructed. 行为的这种变化会影响对象的构造时间。

If we revisit our example above now, with ES6 classes 如果我们现在使用ES6类来回顾上面的示例

class Parent {
  constructor() {
  }
}
class Child extends Parent {
  constructor() {
    super();
  }
}

Child is not "base" , so when the Child constructor initially runs, the this value is uninitialized. Child不是"base" ,因此在Child构造函数最初运行时, this值未初始化。 You can kind of think of super() as doing const this = super(); 您可以将super()视为const this = super(); , so just like ,就像

console.log(value);
const value = 4;

would throw an exception, because value had not been initialized yet, it is the call to super() that calls the parent [[Construct]] , and then initializes the this inside of the Child constructor function body. 会引发异常,因为value尚未初始化,因此对super()调用将调用父级[[Construct]] ,然后在Child构造函数内部将this初始化。 The parent [[Construct]] behaves just like it would in ES5 if it were function Parent(){} , because [[ConstructorKind]] is "base" . 如果父function Parent(){} [[ConstructorKind]]"base" ,则其父[[Construct]]行为与ES5中的function Parent(){}

This behavior is also what allows ES6 class syntax to extend native types like Array . 这种行为也是允许ES6类语法扩展诸如Array类的本机类型的原因。 The call to super() is what actually creates the instance, and since the Array function knows all that it needs to know to create a real functional array, it can do so, and then return that object. super()的调用实际上是创建实例的原因,并且由于Array函数知道创建实际函数数组所需的全部知识,因此它可以做到这一点,然后返回该对象。

In both cases, when is the prototype property set? 在这两种情况下,原型属性何时设置? Is it before or after executing the constructor function / ClassBody? 是在执行构造函数/ ClassBody之前还是之后?

The other key piece that I glossed over above is the exact nature of newTarget mentioned above in the spec snippets. 我上面提到的另一个关键是规范片段中提到的newTarget的确切性质。 In ES6, there is a new concept that is the "new target", which is the actual constructor function passed to new . 在ES6中,有一个新概念,即“新目标”,它是传递给new的实际构造函数。 So if you do new Foo , you're actually using Foo in two different ways. 因此,如果您使用new Foo ,则实际上是以两种不同的方式使用Foo One is that you're using the function as a constructor, but the other is that you're using that value as the "new target". 一种是将函数用作构造函数,而另一种是将值用作“新目标”。 This is critical for the nesting of class constructors, because when you call a chain of [[Construct]] functions, the actual constructor being called will work it's way up the chain, but the newTarget value will remain the same. 这对于嵌套类构造函数至关重要,因为当您调用[[Construct]]函数链时,实际调用的构造函数将沿链向上运行,但是newTarget值将保持不变。 This is important because newTarget.prototype is what is used to actually set the prototype of the final constructed object. 这很重要,因为newTarget.prototype是用于实际设置最终构造对象的原型的东西。 For instance, when you do 例如,当您

class Parent extends Array {
  constructor() {
    console.log(new.target); // Child
    super();
  }
}
class Child extends Parent {
  constructor() {
    console.log(new.target); // Child
    super();
  }
}
new Child();

The call to new Child will call the Child constructor, and also set it as the newTarget value to Child . new Child的调用将调用Child构造函数,并将其作为newTarget值设置为Child Then when super() is called, we're using [[Construct]] from Parent , but also passing Child as the newTarget value still. 然后,当调用super()时,我们使用的是Parent [[Construct]] ,但仍将Child作为newTarget值传递。 This repeats for Parent and means that even though Array is responsible for creating an array exotic object, it can still use newTarget.prototype ( Child.prototype ) to ensure that the array has the correct prototype chain. 这对Parent重复Child.prototype ,这意味着即使Array负责创建数组奇异对象,它仍可以使用newTarget.prototypeChild.prototype )确保该数组具有正确的原型链。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM