Когда новые объекты создаются в функции конструктора JavaScript против класса?

1

Функция конструктора

При создании нового объекта с помощью старой старой конструкторской функции ES5: Когда создается новый объект?

Догадка: создается ли это сразу, когда механизм JS встречает new ключевое слово непосредственно перед выполнением функции конструктора?


Учебный класс

Как и выше, но для классов: когда создается новый объект?

Догадка: поскольку мы можем подклассировать встроенные объекты с синтаксисом class, я думаю, что движок должен знать, какой тип (exotic vs ordinary) является его родительским объектом. Поэтому я думал, что, возможно, новый объект создается правильно, когда движок встречает ключевое слово extends и может читать, какой тип является родителем.


наконец

В обоих случаях, когда задано свойство прототипа? До или после выполнения функции конструктора /ClassBody?


Заметки

Примечание 1: Было бы замечательно, если бы ответ мог включать ссылки на то, где в спецификации ECMAScript происходит каждое из двух созданий. Я много искал и не смог найти правильные шаги алгоритма.

Примечание 2: С "созданным" я имею в виду пространство, выделенное в памяти и набор типов (экзотический или обычный), как минимум.

  • 0
    @JaromandaX. Это не просто синтаксический сахар. Например: 1) С классами мы наследуем статические свойства; 2) С помощью классов мы можем создавать экзотические объекты при расширении других экзотических объектов (например, Array). Это позволяет нам расширять экзотические встроенные функции таким образом, который раньше был невозможен. Поэтому я думаю, что должна быть разница во времени при создании нового объекта. Следующая статья ссылается на это: sitepoint.com/object-oriented-javascript-deep-dive-es6-classes
  • 0
    Поэтому вам не нужно читать все это @JaromandaX: «Классы ES6 исправили это, изменив, когда и кем распределяются объекты ....». Этот параграф упоминает это. Я просто хочу увидеть это сам, из спецификации ECMAScript.
Показать ещё 14 комментариев
Теги:
ecmascript-6
ecmascript-2017

2 ответа

1
Лучший ответ

new вызовет Construct, который, в свою очередь, вызовет связанную функцию internal [[Construct]]. Здесь я буду обсуждать только обычный [[Construct]], и не заботятся о том, что у Proxies есть пользовательское поведение для него, так как это imho не связано с темой.


В стандартном сценарии (без extends) на шаге 5.a, [[Construct]] вызывает OrdinaryCreateFromConstructor, и его возвращение будет использоваться как this (см. OrdinaryCallBindThis, где он используется как аргумент). Обратите внимание, что OrdinaryCallEvaluateBody приходит на более позднем этапе - объект создается, прежде чем функция конструктора будет оценена. Для new f это в основном Object.create(f.prototype). Как правило, это Object.create(newTarget.prototype). Это то же самое для class и ES5. Очевидно, прототип установлен там.


Путаница, вероятно, происходит от случая, где extends используется. В этом случае [[ConstructorKind]] не является "базовым" (см. Шаг 15 ClassDefinitionEvaluation), поэтому в [[Construct]] шаг 5.a больше не применяется и не вызывается OrdinaryCallBindThis. Важная часть здесь происходит в супервызове. Короче говоря, он называет Construct с SuperConstructor и текущим newTarget и связывает результат как this. Соответственно, как вы знаете, любой доступ к this до супервызовов приводит к ошибке. Таким образом, "новый объект" создается в супервызове (обратите внимание, что обсуждаемый снова относится к этому вызову Construct - если SuperConstructor не расширяет ничего, что не происходит, в противном случае это одно - с той лишь разницей, что newTarget).

Чтобы подробнее описать новую переадресацию маршрутов, вот пример того, как это происходит:

class A { constructor() { console.log('newTarget: ${new.target.name}'); } }
class B extends A { constructor(){ super(); } }
console.log(
  'B.prototype prototype: ${Object.getPrototypeOf(B.prototype).constructor.name}.prototype'
);
console.log("Performing 'new A();':");
new A();
console.log("Performing 'new B();':");
new B();

Поскольку [[Construct]] вызывает OrdinaryCreateFromConstructor с параметром newTarget as, который всегда пересылается, используемый прототип будет правильным в конце (в приведенном выше примере, B.prototype, и обратите внимание, что это в свою очередь имеет A.prototype как прототип, aka Object.getPrototypeOf(B.prototype) === A.prototype). Хорошо смотреть на все связанные части (супер-вызов, Construct, [[Construct]] и OrdinaryCreateFromConstructor) и смотреть, как они получают/устанавливают или передают newTarget. Обратите внимание также на то, что вызов PrepareForOrdinaryCall также получает newTarget и устанавливает его в FunctionEnvironment связанных вызовов SuperConstructor, так что дополнительные цепные супервызывные вызовы также получат правильный (в случае расширения от чего-то, что в свою очередь распространяется от что-то).


Последнее, но наименее, конструкторы могут использовать 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 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, что он проверяет: "Если NewTarget не определено [...]" и в противном случае правильно вызывает OrdinaryCreateFromConstructor с помощью NewTarget, добавляя внутренний [[NumberValue]] слот, а затем установите его на следующем шаге.


Изменить, чтобы попытаться ответить на вопросы в комментариях:

Я думаю, вы все еще рассматриваете class и ES5 как две разные вещи. class - почти полностью синтаксический сахар, как уже упоминалось в комментариях к вопросу. Класс - это не что иное, как функция, похожая на "старый путь ES5".


К вашему первому вопросу упоминается "метод", который будет использоваться в ES5 (и что будет удерживать переменная, class A extends Number {}; console.log(typeof A === "function" && Object.getPrototypeOf(A) === Number);). Прототип установлен, чтобы достичь того, что вы ранее отмечали как "наследование статических свойств". Статические свойства - это не что иное, как свойства на конструкторе (если вы когда-либо использовали путь ES5).

[[HomeObject]] используется для доступа к super, как описано в таблице 27. Если вы посмотрите, что делают связанные вызовы (см. Таблицу 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, вызывает [[Construct]] из B с newTarget, теперь установленным на B

  • [[Construct]] встречает вид, который не является "базовым" и, как таковой, не создает никакого объекта

  • PrepareForOrdinaryCall для выполнения конструктора генерирует новый контекст выполнения вместе с новой функциональной средой (где [[NewTarget]] будет установлен на newTarget!) И делает ее текущим контекстом выполнения.

  • OrdinaryCallBindThis также не выполняется, и this остается неинициализированным

  • OrdinaryCallEvaluateBody теперь начнет выполнение конструктора B

  • Супервызов встречается и выполняется:

    • GetNewTarget() извлекает [[NewTarget]] из объекта FunctionEnvironment, который ранее был установлен

    • Construct вызывается на SuperConstructor, с полученным newTarget

    • Он называет [[Construct]] SuperConstructor, с помощью newTarget

    • SuperConstructor имеет своеобразную "базу", поэтому он выполняет OrdinaryCreateFromConstructor, но с набором newTarget. В настоящее время это по существу Object.create(B.prototype) и еще раз отметим, что Object.getPrototypeOf(B.prototype) === A.prototype, уже установленный на функции B, из конструкции класса.

    • Аналогично выше, создается новый контекст выполнения, и на этот раз также выполняется OrdinaryCallBindThis. SuperConstructor выполнит, произведет некоторый объект, контекст выполнения снова появится. Обратите внимание, что если A в свою очередь снова расширит что-то еще, newTarget будет правильно задан везде, поэтому он будет идти глубже и глубже.

    • super берет результат из Construct (объект, созданный SuperConstructor, который имеет прототип B.prototype, не должен ничего исключительного происходить - как обсуждалось, например, конструктор возвращает другое значение, или прототип был изменен вручную) и устанавливает его как this в текущей среде, которая является той, которая используется для выполнения конструктора B (другая уже выскочила).

  • выполнение конструктора B продолжается, с this теперь инициализируется. Это объект, который имеет прототип B.prototype как прототип, который в свою очередь имеет прототип A.prototype как прототип и на котором уже был A.prototype конструктор A (опять же, если ничего исключительного не произошло), так что this.aProp уже существует. Затем конструктор B добавит bProp, и этот объект является результатом new B(); ,

  • 0
    Невероятно полезная Герте, большое спасибо! Я проработаю свой путь и вернусь с любыми вопросами.
  • 0
    Первый вопрос: в ClassDefinitionEvaluation (14.5.13) шаг 12, по-видимому, определяет новый метод, для которого свойство prototype установлено в суперкласс, а [[HomeObject]] установлено в пустой объект, ссылающийся на superclass.prototype. Это правильно? Что должен представлять этот новый метод? Как видно на шаге 14, весь этот новый метод возвращается из ClassDefinitionEvaluation.
Показать ещё 5 комментариев
1

При создании нового объекта с помощью старой старой конструкторской функции ES5: Когда создается новый объект?

Определение уровня построения объекта на уровне спецификации определяется функцией [[Construct]]. Для стандартных функций JS (function Foo(){} определение этой функции инициализируется в 9.2.3 FunctionAllocate, где functionKind будет "normal". Затем вы можете видеть на шаге 9.a, объявлен слот [[Construct]] для указания в разделе 9.2.2 и [[ConstructorKind]] установлено значение "base".

Когда код пользователя вызывает new Foo(); чтобы построить экземпляр этой функции, он вызовет из 12.3.3 new оператор в 12.3.3.1.1 EvaluateNew to 7.3.13 Построить на [[Construct]], который вызывает слот, инициализированный выше, передавая аргументы, и Foo функционирует как newTarget.

Копаем в 9.2.2 [[Construct]], мы видим, что шаг 5.a выполняет:

  1. а. Пусть thisArgument будет? OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%").

который отвечает на ваш вопрос о том, когда. this объект создаются здесь, по существу, делает Object.create(Foo.prototype) (с небольшими дополнительными игнорируемыми логиками там). Затем функция будет продолжена и на шаге 8 она будет выполняться

  1. Если kind является "base", выполните OrdinaryCallBindThis(F, calleeContext, thisArgument).

о котором вы можете как-то подумать this = thisArgument, который установит значение this в функции, прежде чем он на самом деле вызовет логику функции Foo на шаге 11.

Основное отличие классов ES6 от функций конструктора ES5-типа заключается в том, что методы [[Construct]] используются только один раз, на первом уровне построения. Например, если у нас есть

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

new Child();

new будет использовать [[Construct]] для Child, но вызов Parent использует .apply, а это означает, что он на самом деле не строить родителей, он просто назвав его как обычную функцию и проходя по соответствующей this значению.

Здесь все усложняется, как вы заметили, потому что это означает, что Parent фактически не имеет никакого влияния на создание this, и просто должен надеяться, что ему будет предоставлено приемлемое значение.

Как и выше, но для классов: когда создается новый объект?

Основное отличие от синтаксиса класса ES6 состоит в том, что, поскольку родительская функция вызывается с помощью super() вместо Parent.call/Parent.apply, Parent.apply [[Construct]] родительских функций вызывается вместо [[Call]]. Из-за этого на самом деле возможно попасть в 9.2.2 [[Construct]] с [[ConstructorKind]] установленным на нечто иное, чем "base". Это изменение в поведении, которое влияет на построение объекта.

Если мы снова рассмотрим наш пример выше, с классами ES6

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]] ведет себя так же, как в ES5, если это function Parent(){}, потому что [[ConstructorKind]] является "base".

Такое поведение также позволяет синтаксису класса ES6 расширять собственные типы, такие как Array. Вызов super() - это то, что на самом деле создает экземпляр, и поскольку функция Array знает все, что ему нужно знать, чтобы создать реальный функциональный массив, он может это сделать, а затем вернуть этот объект.

В обоих случаях, когда задано свойство прототипа? До или после выполнения функции конструктора /ClassBody?

Другим ключевым элементом, который я затушевывал выше, является точная природа newTarget упомянутая выше в newTarget спецификации. В ES6 существует новая концепция, которая является "новой целью", которая является фактической функцией конструктора, переданной 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, а также установите для него значение newTarget для Child. Затем, когда вызывается super(), мы используем [[Construct]] из Parent, но также newTarget Child как значение newTarget. Это повторяется для Parent и означает, что хотя Array отвечает за создание экзотического объекта массива, он все равно может использовать newTarget.prototype (Child.prototype), чтобы гарантировать, что массив имеет правильную цепочку прототипов.

  • 0
    Спасибо, Логан, как всегда высоко ценю. Мне нужно время, чтобы проработать все это.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню