При создании нового объекта с помощью старой старой конструкторской функции ES5: Когда создается новый объект?
Догадка: создается ли это сразу, когда механизм JS встречает new
ключевое слово непосредственно перед выполнением функции конструктора?
Как и выше, но для классов: когда создается новый объект?
Догадка: поскольку мы можем подклассировать встроенные объекты с синтаксисом class
, я думаю, что движок должен знать, какой тип (exotic
vs ordinary
) является его родительским объектом. Поэтому я думал, что, возможно, новый объект создается правильно, когда движок встречает ключевое слово extends
и может читать, какой тип является родителем.
В обоих случаях, когда задано свойство прототипа? До или после выполнения функции конструктора /ClassBody?
Примечание 1: Было бы замечательно, если бы ответ мог включать ссылки на то, где в спецификации ECMAScript происходит каждое из двух созданий. Я много искал и не смог найти правильные шаги алгоритма.
Примечание 2: С "созданным" я имею в виду пространство, выделенное в памяти и набор типов (экзотический или обычный), как минимум.
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();
,
При создании нового объекта с помощью старой старой конструкторской функции 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
выполняет:
- а. Пусть
thisArgument
будет?OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%")
.
который отвечает на ваш вопрос о том, когда. this
объект создаются здесь, по существу, делает Object.create(Foo.prototype)
(с небольшими дополнительными игнорируемыми логиками там). Затем функция будет продолжена и на шаге 8
она будет выполняться
- Если 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
), чтобы гарантировать, что массив имеет правильную цепочку прототипов.