Почему необходимо установить конструктор прототипа?
В разделе о наследовании в статье MDN Введение в объектно-ориентированный Javascript я заметил, что они установили prototype.constructor:
// correct the constructor pointer because it points to Person
Student.prototype.constructor = Student;
Служит ли это какой-либо важной цели? Можно ли это опустить?
12 ответов
Это не всегда необходимо, но у него есть свое применение. Предположим, мы хотели сделать метод копирования на основе Person
учебный класс. Как это:
// define the Person Class
function Person(name) {
this.name = name;
}
Person.prototype.copy = function() {
// return new Person(this.name); // just as bad
return new this.constructor(this.name);
};
// define the Student class
function Student(name) {
Person.call(this, name);
}
// inherit Person
Student.prototype = Object.create(Person.prototype);
Теперь, что происходит, когда мы создаем новый Student
и скопировать это?
var student1 = new Student("trinth");
console.log(student1.copy() instanceof Student); // => false
Копия не является экземпляром Student
, Это потому, что (без явных проверок) у нас не было бы возможности вернуть Student
копия из "базового" класса. Мы можем только вернуть Person
, Однако, если мы сбросили конструктор:
// correct the constructor pointer because it points to Person
Student.prototype.constructor = Student;
... тогда все работает как положено:
var student1 = new Student("trinth");
console.log(student1.copy() instanceof Student); // => true
Служит ли это какой-либо важной цели?
И да и нет.
В ES5 и более ранних версиях сам JavaScript не использовался constructor
для всего. Определено, что объект по умолчанию для функции prototype
свойство будет иметь его, и оно будет ссылаться на функцию, и это было все. Ничто другое в спецификации не ссылается на это вообще.
Это изменилось в ES2015 (ES6), который начал использовать его в отношении иерархий наследования. Например, Promise#then
использует constructor
свойство обещания, которое вы вызываете (через SpeciesConstructor) при создании нового обещания для возврата. Он также участвует в подтипировании массивов (через ArraySpeciesCreate).
За пределами самого языка иногда люди используют его при попытке создания универсальных функций "клонирования" или просто в целом, когда они хотят сослаться на то, что, по их мнению, будет функцией конструктора объекта. Мой опыт показывает, что использовать его редко, но иногда люди используют его.
Можно ли это опустить?
Это там по умолчанию, вам нужно только вернуть его, когда вы заменяете объект в функции prototype
имущество:
Student.prototype = Object.create(Person.prototype);
Если вы этого не сделаете:
Student.prototype.constructor = Student;
...затем Student.prototype.constructor
наследуется от Person.prototype
который (предположительно) имеет constructor = Person
, Так что это вводит в заблуждение. И, конечно, если вы создаете подкласс того, что использует его (например, Promise
или же Array
) и не использую class
Which (который обрабатывает это для вас), вы хотите убедиться, что вы установили его правильно. Итак, в основном: это хорошая идея.
Это нормально, если ничто в вашем коде (или коде библиотеки, который вы используете) не использует его. Я всегда следил, чтобы он был правильно подключен.
Конечно, с ES2015 (он же ES6) class
ключевое слово, большую часть времени мы бы использовали его, нам больше не нужно, потому что он обрабатывается для нас, когда мы делаем
class Student extends Person {
}
¹ "... если вы создаете подкласс того, что его использует (например, Promise
или же Array
) и не использую class
... " - Это возможно сделать, но это настоящая боль (и немного глупо). Вы должны использовать Reflect.construct
,
TLDR; Не супер необходим, но, вероятно, поможет в долгосрочной перспективе, и это более точно.
ПРИМЕЧАНИЕ. Многое отредактировано, так как мой предыдущий ответ был сбит с толку и содержал некоторые ошибки, которые я упустил, пытаясь ответить. Спасибо тем, кто указал на некоторые вопиющие ошибки.
По сути, это правильно подключить подклассы в Javascript. Когда мы создаем подкласс, мы должны сделать несколько забавных вещей, чтобы убедиться, что прототипное делегирование работает правильно, включая перезапись prototype
объект. Перезапись prototype
объект включает в себя constructor
поэтому нам нужно исправить ссылку.
Давайте быстро рассмотрим, как работают "классы" в ES5.
Допустим, у вас есть функция конструктора и ее прототип:
//Constructor Function
var Person = function(name, age) {
this.name = name;
this.age = age;
}
//Prototype Object - shared between all instances of Person
Person.prototype = {
species: 'human',
}
Когда вы вызываете конструктор для создания экземпляра, говорите Adam
:
// instantiate using the 'new' keyword
var adam = new Person('Adam', 19);
new
Ключевое слово, вызываемое с помощью "Person", в основном запускает конструктор Person с несколькими дополнительными строками кода:
function Person (name, age) {
// This additional line is automatically added by the keyword 'new'
// it sets up the relationship between the instance and the prototype object
// So that the instance will delegate to the Prototype object
this = Object.create(Person.prototype);
this.name = name;
this.age = age;
return this;
}
/* So 'adam' will be an object that looks like this:
* {
* name: 'Adam',
* age: 19
* }
*/
Если мы console.log(adam.species)
, поиск не удастся на adam
экземпляр, и посмотрите цепочку прототипов к ее .prototype
, который Person.prototype
- а также Person.prototype
имеет .species
свойство, поэтому поиск будет успешным в Person.prototype
, Затем он войдет 'human'
,
Здесь Person.prototype.constructor
будет правильно указывать на Person
,
Итак, теперь интересная часть, так называемый "подкласс". Если мы хотим создать Student
класс, который является подклассом Person
класс с некоторыми дополнительными изменениями, мы должны убедиться, что Student.prototype.constructor
указывает на студента для точности.
Это не делает это само по себе. Когда вы создаете подкласс, код выглядит так:
var Student = function(name, age, school) {
// Calls the 'super' class, as every student is an instance of a Person
Person.call(this, name, age);
// This is what makes the Student instances different
this.school = school
}
var eve = new Student('Eve', 20, 'UCSF');
console.log(Student.prototype); // this will be an empty object: {}
призвание new Student()
Здесь будет возвращен объект со всеми свойствами, которые мы хотим. Здесь, если мы проверим eve instanceof Person
, он бы вернулся false
, Если мы попытаемся получить доступ eve.species
, он бы вернулся undefined
,
Другими словами, нам нужно подключить делегацию так, чтобы eve instanceof Person
возвращает истину и так, что случаи Student
правильно делегировать Student.prototype
, а потом Person.prototype
,
НО, так как мы называем это с new
ключевое слово, помните, что добавляет этот вызов? Было бы позвонить Object.create(Student.prototype)
, как мы создали эти делегатские отношения между Student
а также Student.prototype
, Обратите внимание, что прямо сейчас, Student.prototype
пустой. Итак, глядя вверх .species
экземпляр Student
потерпит неудачу, так как делегирует только Student.prototype
и .species
собственность не существует на Student.prototype
,
Когда мы назначаем Student.prototype
в Object.create(Person.prototype)
, Student.prototype
Сам тогда делегаты Person.prototype
и глядя вверх eve.species
вернусь human
как мы ожидаем. Предположительно мы хотим, чтобы он наследовал от Student.prototype AND Person.prototype. Поэтому нам нужно все это исправить.
/* This sets up the prototypal delegation correctly
*so that if a lookup fails on Student.prototype, it would delegate to Person's .prototype
*This also allows us to add more things to Student.prototype
*that Person.prototype may not have
*So now a failed lookup on an instance of Student
*will first look at Student.prototype,
*and failing that, go to Person.prototype (and failing /that/, where do we think it'll go?)
*/
Student.prototype = Object.create(Person.prototype);
Сейчас делегация работает, но мы перезаписываем Student.prototype
с из Person.prototype
, Так что если мы позвоним Student.prototype.constructor
, это будет указывать на Person
вместо Student
, Вот почему мы должны это исправить.
// Now we fix what the .constructor property is pointing to
Student.prototype.constructor = Student
// If we check instanceof here
console.log(eve instanceof Person) // true
В ES5 наш constructor
Свойство - это ссылка, которая ссылается на функцию, которую мы написали с намерением стать "конструктором". Помимо того, что new
Ключевое слово дает нам, в противном случае конструктор является "простой" функцией.
В ES6 constructor
теперь встроен в способ, которым мы пишем классы - например, он предоставляется как метод, когда мы объявляем класс. Это просто синтаксический сахар, но он предоставляет нам некоторые удобства, такие как доступ к super
когда мы расширяем существующий класс. Поэтому мы бы написали приведенный выше код так:
class Person {
// constructor function here
constructor(name, age) {
this.name = name;
this.age = age;
}
// static getter instead of a static property
static get species() {
return 'human';
}
}
class Student extends Person {
constructor(name, age, school) {
// calling the superclass constructor
super(name, age);
this.school = school;
}
}
Я бы не согласился. Нет необходимости устанавливать прототип. Возьмите тот же самый код, но удалите строку prototype.constructor. Что-нибудь меняется? Теперь внесите следующие изменения:
Person = function () {
this.favoriteColor = 'black';
}
Student = function () {
Person.call(this);
this.favoriteColor = 'blue';
}
и в конце кода теста...
alert(student1.favoriteColor);
Цвет будет синим.
По моему опыту, изменение в prototype.constructor мало что даст, если вы не делаете очень конкретные, очень сложные вещи, которые, вероятно, в любом случае не являются хорошей практикой:)
Редактировать: После того, как вы покопались в сети и немного поэкспериментировали, похоже, что люди настраивают конструктор так, чтобы он "выглядел" как то, что создается с помощью "new". Думаю, я бы сказал, что проблема в том, что javascript является языком-прототипом - нет такой вещи, как наследование. Но большинство программистов происходят из опыта программирования, который выдвигает наследование как "путь". Таким образом, мы придумываем всевозможные вещи, чтобы попытаться сделать этот язык-прототип "классическим" языком… например, расширение "классов". Действительно, в приведенном ими примере новый ученик - это человек - он не "расширяется" от другого ученика… ученик - это все о человеке, и кем бы ни был этот ученик. Расширьте студента, и все, что вы расширили, - это студент в глубине души, но он настроен под ваши нужды.
Крокфорд немного сумасшедший и излишне усердный, но внимательно прочитайте кое-что из написанного им... это заставит вас взглянуть на это совсем по-другому.
Это огромная ловушка, если вы написали
Student.prototype.constructor = Student;
но тогда, если был Учитель, чей прототип был также Человек, и вы написали
Teacher.prototype.constructor = Teacher;
тогда ученик конструктор теперь учитель!
Редактировать: вы можете избежать этого, убедившись, что вы установили прототипы учеников и учителей, используя новые экземпляры класса Person, созданные с помощью Object.create, как в примере Mozilla.
Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);
До сих пор путаница все еще там.
Следуя исходному примеру, поскольку у вас есть существующий объект student1
как:
var student1 = new Student("Janet", "Applied Physics");
Предположим, вы не хотите знать, как student1
создан, вы просто хотите другой объект, как это, вы можете использовать свойство конструктора student1
лайк:
var student2 = new student1.constructor("Mark", "Object-Oriented JavaScript");
Здесь он не сможет получить свойства от Student
если свойство конструктора не установлено. Скорее это создаст Person
объект.
Получил хороший пример кода того, почему действительно необходимо установить конструктор прототипа.
function CarFactory(name){
this.name=name;
}
CarFactory.prototype.CreateNewCar = function(){
return new this.constructor("New Car "+ this.name);
}
CarFactory.prototype.toString=function(){
return 'Car Factory ' + this.name;
}
AudiFactory.prototype = new CarFactory(); // Here's where the inheritance occurs
AudiFactory.prototype.constructor=AudiFactory; // Otherwise instances of Audi would have a constructor of Car
function AudiFactory(name){
this.name=name;
}
AudiFactory.prototype.toString=function(){
return 'Audi Factory ' + this.name;
}
var myAudiFactory = new AudiFactory('');
alert('Hay your new ' + myAudiFactory + ' is ready.. Start Producing new audi cars !!! ');
var newCar = myAudiFactory.CreateNewCar(); // calls a method inherited from CarFactory
alert(newCar);
/*
Without resetting prototype constructor back to instance, new cars will not come from New Audi factory, Instead it will come from car factory ( base class ).. Dont we want our new car from Audi factory ????
*/
Это необходимо, когда вам нужна альтернатива toString
без обезьяноподготовки:
//Local
foo = [];
foo.toUpperCase = String(foo).toUpperCase;
foo.push("a");
foo.toUpperCase();
//Global
foo = [];
window.toUpperCase = function (obj) {return String(obj).toUpperCase();}
foo.push("a");
toUpperCase(foo);
//Prototype
foo = [];
Array.prototype.toUpperCase = String.prototype.toUpperCase;
foo.push("a");
foo.toUpperCase();
//toString alternative via Prototype constructor
foo = [];
Array.prototype.constructor = String.prototype.toUpperCase;
foo.push("a,b");
foo.constructor();
//toString override
var foo = [];
foo.push("a");
var bar = String(foo);
foo.toString = function() { return bar.toUpperCase(); }
foo.toString();
//Object prototype as a function
Math.prototype = function(char){return Math.prototype[char]};
Math.prototype.constructor = function()
{
var i = 0, unicode = {}, zero_padding = "0000", max = 9999;
while (i < max)
{
Math.prototype[String.fromCharCode(parseInt(i, 16))] = ("u" + zero_padding + i).substr(-4);
i = i + 1;
}
}
Math.prototype.constructor();
console.log(Math.prototype("a") );
console.log(Math.prototype["a"] );
console.log(Math.prototype("a") === Math.prototype["a"]);
В наши дни не нужно использовать классы с сахаром или использовать "New". Используйте объектные литералы.
Прототип Object уже является "классом". Когда вы определяете литерал объекта, он уже является экземпляром объекта-прототипа. Они также могут выступать в качестве прототипа другого объекта и т. Д.
const Person = {
name: '[Person.name]',
greeting: function() {
console.log( `My name is ${ this.name || '[Name not assigned]' }` );
}
};
// Person.greeting = function() {...} // or define outside the obj if you must
// Object.create version
const john = Object.create( Person );
john.name = 'John';
console.log( john.name ); // John
john.greeting(); // My name is John
// Define new greeting method
john.greeting = function() {
console.log( `Hi, my name is ${ this.name }` )
};
john.greeting(); // Hi, my name is John
// Object.assign version
const jane = Object.assign( Person, { name: 'Jane' } );
console.log( jane.name ); // Jane
// Original greeting
jane.greeting(); // My name is Jane
// Original Person obj is unaffected
console.log( Person.name ); // [Person.name]
console.log( Person.greeting() ); // My name is [Person.name]
Основанные на классах объектно-ориентированные языки, такие как Java и C++, основаны на концепции двух разных сущностей: классов и экземпляров.
...
Язык на основе прототипов, такой как JavaScript, не делает этого различия: у него просто есть объекты. Основанный на прототипе язык имеет понятие прототипного объекта, объекта, используемого в качестве шаблона, из которого можно получить начальные свойства для нового объекта. Любой объект может указывать свои собственные свойства, либо при его создании, либо во время выполнения. Кроме того, любой объект может быть связан как прототип для другого объекта, что позволяет второму объекту совместно использовать свойства первого объекта.
Это необходимо. Любой класс в наследовании классов должен иметь собственный конструктор, как и в наследовании прототипов. Это также удобно для построения объектов. Но вопрос излишний, и что необходимо, так это понимание в мире JavaScript эффекта вызова функции как конструктора и правила разрешения свойства объекта.
Эффект выполнения функции как конструктора с выражением new <имя функции>([параметры])
- создается объект, имя типа которого является именем функции
- внутренние свойства в функции прикрепляются к созданному объекту
- прототип свойства функции автоматически присоединяется к созданному объекту как прототип
Правило разрешающего свойства объекта
- Свойство будет искать не только в объекте, но и в прототипе объекта, прототипе прототипа и так далее, пока либо не будет найдено свойство с совпадающим именем, либо не будет достигнут конец цепочки прототипов.
Основываясь на этих базовых механизмах, оператор <имя конструктора>.prototype.constructor = <имя конструктора>по своему действию равен присоединению конструктора в теле конструктора с выражением this.constructor = <имя конструктора>. Конструктор будет разрешен для объекта, если второе высказывание, а для прототипа объекта, если первое высказывание.
РЕДАКТИРОВАТЬ, я был на самом деле не так. Комментирование строки не меняет ее поведение вообще. (Я проверял это)
Да, это необходимо. Когда вы делаете
Student.prototype = new Person();
Student.prototype.constructor
становится Person
, Поэтому звоню Student()
вернет объект, созданный Person
, Если вы тогда делаете
Student.prototype.constructor = Student;
Student.prototype.constructor
сбрасывается обратно в Student
, Теперь, когда вы звоните Student()
он выполняет Student
, который вызывает родительский конструктор Parent()
, он возвращает правильно унаследованный объект. Если вы не сбросили Student.prototype.constructor
перед его вызовом вы получите объект, который не будет иметь никаких свойств, установленных в Student()
,
Дана простая функция конструктора:
function Person(){
this.name = 'test';
}
console.log(Person.prototype.constructor) // function Person(){...}
Person.prototype = { //constructor in this case is Object
sayName: function(){
return this.name;
}
}
var person = new Person();
console.log(person instanceof Person); //true
console.log(person.sayName()); //test
console.log(Person.prototype.constructor) // function Object(){...}
По умолчанию (из спецификации https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/constructor) все прототипы автоматически получают свойство с именем constructor, которое указывает на функцию который является собственностью. В зависимости от конструктора, к прототипу могут быть добавлены другие свойства и методы, что не является обычной практикой, но все же допускается для расширений.
Итак, просто отвечая: нам нужно убедиться, что значение в prototype.constructor установлено правильно, как предполагается в спецификации.
Должны ли мы всегда правильно устанавливать это значение? Это помогает в отладке и делает внутреннюю структуру согласованной со спецификацией. Мы должны определенно, когда наш API используется третьими сторонами, но не совсем, когда код наконец выполняется во время выполнения.
Вот один пример из MDN, который я нашел очень полезным для понимания его использования.
В JavaScript мы имеем async functions
который возвращает объект AsyncFunction. AsyncFunction
не является глобальным объектом, но его можно получить с помощью constructor
имущество и использовать его.
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
// AsyncFunction constructor
var AsyncFunction = Object.getPrototypeOf(async function(){}).constructor
var a = new AsyncFunction('a',
'b',
'return await resolveAfter2Seconds(a) + await resolveAfter2Seconds(b);');
a(10, 20).then(v => {
console.log(v); // prints 30 after 4 seconds
});
Это не обязательно. Чемпионы ООП стараются превратить прототипическое наследование JavaScript в классическое наследование - это лишь одна из многих традиционных вещей. Единственное что следующее
Student.prototype.constructor = Student;
делает, что теперь у вас есть ссылка на текущий "конструктор".
В ответе Уэйна, который был помечен как правильный, вы можете сделать то же самое, что и следующий код
Person.prototype.copy = function() {
// return new Person(this.name); // just as bad
return new this.constructor(this.name);
};
с кодом ниже (просто замените this.constructor на Person)
Person.prototype.copy = function() {
// return new Person(this.name); // just as bad
return new Person(this.name);
};
Слава Богу, что с классическим наследованием ES6 пуристы могут использовать нативные операторы языка, такие как class, extends и super, и нам не нужно видеть подобные как prototype.constructor исправления и родительские ссылки.