代码之家  ›  专栏  ›  技术社区  ›  Magnus

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

  •  0
  • Magnus  · 技术社区  · 7 年前

    构造函数

    当通过一个好的旧ES5构造函数创建一个新对象时:什么时候创建新对象?

    猜测 :当JS引擎遇到 new 关键字,直接在执行构造函数函数之前?


    等级

    与上面类似,但对于类:何时创建新对象?

    猜测 :因为我们可以使用 class 语法,我想引擎必须知道什么类型( exotic ordinary )它的父对象是。因此,我在想,当引擎遇到 extends 关键字,可以读取父对象的类型。


    最后

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


    笔记

    注1 :如果答案中包含指向 ECMAScript specification 两个创造物中的每一个都发生了。我一直在四处寻找,一直找不到正确的算法步骤。

    注2 :对于“created”,我指的是至少在内存和类型集中分配的空间(外来的和普通的)。

    2 回复  |  直到 7 年前
        1
  •  2
  •   ASDFGerte    7 年前

    new 将调用 Construct ,它将依次调用相关函数的内部 [[Construct]] . 我只讨论这里的[ [构造] ],而不关心代理,例如它有自定义行为,因为这与主题无关。


    在标准场景中(否 extends ),在步骤5.a中,[[Construct]]调用 OrdinaryCreateFromConstructor ,其返回值将用作 this (见 OrdinaryCallBindThis ,其中它用作参数)。请注意,OrdinaryCallEvaluateBody在后面的步骤中出现—在对构造函数求值之前创建对象。为了 new f ,基本上是 Object.create(f.prototype) . 一般来说 Object.create(newTarget.prototype) . 这是相同的 class 还有ES5方式。原型机显然也安装在那里。


    这种困惑可能源于 延伸 正在使用。在这种情况下,[[ConstructorKind]不是“base”(参见第15步 ClassDefinitionEvaluation ),因此在[Construct]]中,步骤5.a不再应用,OrdinaryCallBindThis也不再调用。这里的重要部分发生在 super call . 长话短说,它用超级结构和当前新目标调用Construct,并将结果绑定为 . 因此,如你所知,任何 在超级调用导致错误之前。因此,“new object”是在super调用中创建的(请注意,所讨论的再次应用于构造调用-如果super构造函数没有扩展任何内容,则为非派生情况,否则为这个情况-唯一的区别是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]]调用orderycreatefromconstructor,newTarget作为参数,该参数始终被转发,因此使用的原型将是最后正确的原型(在上面的示例中, B.prototype ,并注意到这反过来 A.prototype 作为原型,aka Object.getPrototypeOf(B.prototype) === A.prototype ). 最好看看所有相关的部分(super call、Construct、[[Construct]]和OrdinaryCreateFromConstructor),看看它们是如何获得/设置或传递newTarget的。这里还要注意,对PrepareForOrdinaryCall的调用也得到了newTarget,并将其设置在相关超结构调用的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.

    注:我也是第一次在说明书中读到很多,所以可能会有一些错误。我自己的兴趣是找出扩展内置组件是如何工作的(源于一段时间前的另一场争论)。要理解这一点,在上述内容之后,只需要最后一件事:我们注意到 Number constructor ,检查是否“如果NewTarget未定义[…]”,否则将正确调用OrdinalCreateFromConstructor和NewTarget,同时添加内部的[NumberValue]]插槽,然后在下一步中设置它。


    编辑以尝试回答评论中的问题:

    我想你还在看 和ES5一样是两个独立的东西。 几乎完全是句法上的糖,正如在对这个问题的评论中已经提到的。类只不过是一个函数,类似于“旧的ES5方式”。


    对于你的第一个问题,你提到的“方法”是一个函数,它将以ES5的方式使用(变量将保存什么, class A extends Number {}; console.log(typeof A === "function" && Object.getPrototypeOf(A) === Number); ). 原型已经设置好,以实现您前面提到的“继承静态属性”。静态属性只不过是构造函数上的属性(如果您曾经使用过ES5方法)。

    [[HomeObject]]用于访问 超级的 ,如中所述 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(); 正在评估:

    • 新的 calls Construct,由于没有当前的newTarget,它调用了 B 新目标设置为 .

    • [[Construct]]遇到一种不是“base”的类型,因此不会创建任何对象

    • PrepareForOrdinaryCall,用于执行构造函数,生成一个新的执行上下文,以及一个新的FunctionEnvironment(其中,[[NewTarget]]将设置为NewTarget!),并使其成为正在运行的执行上下文。

    • orderyCallBind也不执行此操作,并且 保持未初始化状态

    • OrdinaryCallEvaluateBody现在将开始执行

    • 遇到并执行超级调用:

      • GetNewTarget()从先前设置的FunctionEnvironment中检索[[NewTarget]]

      • 构造是在超结构上调用的,而检索到的新目标

      • 它调用超级结构的[[构造]],使用newTarget

      • 超级构造函数具有“base”类,因此它执行orderyCreateFromConstructor,但具有newTarget集。这是现在的本质 Object.create(B.prototype) ,请再次注意 Object.getPrototypeOf(B.prototype)==原型 ,已经在函数上设置了 ,来自班级建设。

      • 与上面类似,正在创建一个新的执行上下文,这次,orderyCallBindThis也完成了。超级构造函数将执行,生成一些对象,再次弹出执行上下文。注意应该 A 反过来又扩展了一些其他的东西,newTarget又被正确地设置在任何地方,所以它会越来越深。

      • super从constructor(超级构造函数生成的对象)中获取结果 B、 原型 作为prototype,如果没有异常发生(如所讨论的,构造函数返回一些其他值,或者prototype被手动更改),并将其设置为 在当前环境中,用于执行 (另一个已经被弹出)。

    • 的构造函数的执行 继续,用 现在初始化。它是一个物体 B、 原型 作为原型,反过来 A、 原型 作为原型 一个 构造函数已经被调用(同样,如果没有发生异常情况的话),所以 this.aProp 已经存在。的构造器 然后将添加 bProp ,而该对象是 新B(); .

        2
  •  2
  •   loganfsmyth    7 年前

    当通过一个好的旧ES5构造函数创建一个新对象时:什么时候创建新对象?

    对象构造行为的规范级定义由 [[Construct]] 功能。对于标准JS函数( function Foo(){} ,此函数的定义在 9.2.3 FunctionAllocate 哪里 functionKind "normal" . 然后你可以看到台阶上 9.a ,和 [[构造]] 槽声明为指向 section 9.2.2 [[ConstructorKind]] 设置为 "base" .

    当用户代码调用时 new Foo(); 要构造此函数的实例,它将调用 12.3.3 The new operator 12.3.3.1.1 EvaluateNew 7.3.13 Construct [[构造]] ,它调用上面初始化的插槽,传递参数,以及 Foo 作为 newTarget .

    挖掘 9.2.2 [[Construct]] ,我们可以看到这一步 5.a 执行:

    1. a、 让 thisArgument 是吗? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%") .

    回答了你的问题 什么时候 . 这个 this 对象是在这里创建的 Object.create(Foo.prototype) (里面有一点不可忽视的逻辑)。然后函数将继续并按步骤执行 8 就行了

    1. 如果种类是 “基地” ,执行 OrdinaryCallBindThis(F, calleeContext, thisArgument) .

    你可以把它想象成 this = thisArgument ,它将设置 在函数中,在它真正调用 步进功能 11 .

    ES6类与ES5样式构造函数的主要区别在于 [[构造]] 方法只使用一次,在 第一 建设水平。例如,如果我们有

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

    这个 新的 将使用 [[构造]] 对于 Child ,但是 Parent 使用 .apply ,这意味着它实际上并没有构造父函数,只是像普通函数一样调用它,并传递一个适当的 价值。

    这就是事情变得复杂的地方,正如你所注意到的,因为它意味着 起源 实际上对 ,只希望它被赋予一个可接受的值。

    与上面类似,但对于类:何时创建新对象?

    ES6类语法的主要区别在于 super() 而不是 Parent.call / Parent.apply ,和 [[构造]] 调用父函数的函数,而不是 [[Call]] . 正因为如此,我们才有可能 9.2.2条 [[构造]] 具有 [[ConstructorKind]] 设置为除 “基地” . 正是这种行为的改变影响了对象的构建。

    如果我们现在重新回顾上面的例子,使用ES6类

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

    儿童 不是 “基地” ,所以当 儿童 构造函数最初运行时 值未初始化。你可以想想 超级() 像这样 const this = super(); ,所以就像

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

    会抛出异常,因为 value 尚未初始化,调用 超级() 那叫父母 [[构造]] ,然后初始化 儿童 构造函数函数体。父母 [[构造]] 行为就像在ES5中一样 function Parent(){} ,因为 [[ConstructorKind]] “基地” .

    这种行为还允许ES6类语法扩展本机类型,如 Array . 对…的呼唤 超级() 是实际创建实例的,并且 阵列 函数知道创建真正的函数数组所需知道的一切,它可以这样做,然后返回该对象。

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

    我在上面略过的另一个关键点是 新目标 在规范片段中提到。在ES6中,有一个新的概念是“新目标”,它是传递给 新的 . 如果你这么做了 new Foo ,你实际上在使用 有两种不同的方式。一个是将函数用作构造函数,另一个是将该值用作“新目标”。这对于类构造函数的嵌套非常重要,因为当您调用 [[构造]] 函数,被调用的实际构造函数将在链的上游工作,但是 新目标 值将保持不变。这很重要,因为 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.prototype )以确保阵列具有正确的原型链。