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

时间:2018-08-08 23:35:35

标签: javascript ecmascript-6 ecmascript-2017

构造函数

通过良好的旧ES5构造函数创建新对象时:新对象何时创建?

一个猜测:是在JS引擎遇到new关键字后立即在构造函数执行之前立即创建的吗?


课程

与上述类似,但对于类:什么时候创建新对象?

一个猜测:由于我们可以使用class语法对内置对象进行子类化,因此我认为引擎必须知道哪种类型(exoticordinary )其父对象是。因此,我在想,也许当引擎遇到extends关键字并且可以读取父对象是什么类型时,才创建新对象。


最后

在两种情况下,何时设置原型属性?是在执行构造函数/ ClassBody之前还是之后?


注释

注释1 :如果答案中可以包含指向两个创建中的每个ECMAScript specification的位置的链接,那就太好了。我一直在搜索很多东西,却找不到正确的算法步骤。

注释2 :“创建”是指在内存和类型集(异国与普通)中分配的空间最少。

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 27GetSuperBase),则实际上,您会注意到“ [[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的构造函数的(另一个已经弹出)。

      < / li>
  • 继续执行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 operator12.3.3.1.1 EvaluateNew7.3.13 Construct[[Construct]]进行调用,这会调用上面初始化的插槽,传递参数,而Foo的作用为newTarget

深入到9.2.2 [[Construct]],我们可以看到步骤5.a执行以下操作:

  
      
  1. a。让thisArgument为? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%")
  2.   

回答您的何时问题。 this对象实际上是通过执行Object.create(Foo.prototype)(在其中带有一些额外的可忽略逻辑)来创建的。然后,该函数将继续执行,并在步骤8中执行

  
      
  1. 如果种类为"base",请执行OrdinaryCallBindThis(F, calleeContext, thisArgument)
  2.   

您可能会想到做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负责创建数组奇异对象,它仍可以使用ParentArray)来确保该数组具有正确的原型链。