Почему необходимо установить конструктор прототипа?

В разделе о наследовании в статье 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 <имя функции>([параметры])

  1. создается объект, имя типа которого является именем функции
  2. внутренние свойства в функции прикрепляются к созданному объекту
  3. прототип свойства функции автоматически присоединяется к созданному объекту как прототип

Правило разрешающего свойства объекта

  • Свойство будет искать не только в объекте, но и в прототипе объекта, прототипе прототипа и так далее, пока либо не будет найдено свойство с совпадающим именем, либо не будет достигнут конец цепочки прототипов.

Основываясь на этих базовых механизмах, оператор <имя конструктора>.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 исправления и родительские ссылки.

Другие вопросы по тегам