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

В разделе о наследовании в статье MDN Введение в объектно-ориентированный Javascript я заметил, что они установили prototype.constructor:

// correct the constructor pointer because it points to Person
Student.prototype.constructor = Student;  

Это служит какой-либо важной цели? Можно ли опустить его?

Ответ 1

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

// 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

Ответ 2

Соответствует ли это какой-либо важной цели?

Да и нет.

В ES5 и ранее JavaScript сам не использовал constructor для чего-либо. Он определил, что объект по умолчанию для свойства функции prototype будет иметь его и что он будет ссылаться на функцию, и это все. Ничего другого в спецификации не упоминалось вообще.

Это изменилось в ES2015 (ES6), который начал использовать его по отношению к иерархиям наследования. Например, Promise#then использует свойство constructor обещания, которое вы его вызываете (через SpeciesConstructor) при создании нового обещания вернуться. Он также участвует в подтипировании массивов (через ArraySpeciesCreate).

Вне самого языка иногда люди будут использовать его при попытке создания общих функций "клонировать" или просто вообще, когда они хотят ссылаться на то, что, по их мнению, будет функцией конструктора объектов. Мой опыт в том, что использование его редко, но иногда люди его используют.

Можно ли опустить его?

Он там по умолчанию вам нужно вернуть его только при замене объекта на свойство prototype:

Student.prototype = Object.create(Person.prototype);

Если вы этого не сделаете:

Student.prototype.constructor = Student;

... then Student.prototype.constructor наследует от Person.prototype, который (предположительно) имеет constructor = Person. Так что это вводит в заблуждение. И, конечно, если вы подклассифицируете что-то, что использует его (например, Promise или Array), а не используя class ¹ (который обрабатывает это для вас), вы должны убедиться, что вы правильно настроили его. Итак, в основном: это хорошая идея.

Это нормально, если ничего в коде (или используемом вами библиотечном коде) не использует. Я всегда гарантировал, что он правильно подключен.

Конечно, с ключевым словом ES2015 (иначе ES6) class, большую часть времени мы бы использовали его, нам больше не нужно, потому что он обрабатывался для нас, когда мы делаем

class Student extends Person {
}

¹ "... если вы подклассифицируете что-то, что использует его (например, Promise или Array), а не используя class..." — Это можно сделать, но это настоящая боль (и немного глупо). Вы должны использовать Reflect.construct.

Ответ 3

Я бы не согласился. Нет необходимости устанавливать прототип. Возьмите тот же самый код, но удалите строку prototype.constructor. Что-нибудь изменилось? Нет. Теперь сделайте следующие изменения:

Person = function () {
    this.favoriteColor = 'black';
}

Student = function () {
    Person.call(this);
    this.favoriteColor = 'blue';
}

и в конце тестового кода...

alert(student1.favoriteColor);

Цвет будет синим.

Изменение прототипа. Конструктор, по моему опыту, мало что делает, если вы не делаете очень конкретные, очень сложные вещи, которые, вероятно, не являются хорошей практикой:)

Изменить: После того, как несколько раз выкарабкались по сети и делали некоторые эксперименты, похоже, что люди устанавливают конструктор так, что он "выглядит" как вещь, которая строится с помощью "нового". Думаю, я бы сказал, что проблема в том, что javascript - это прототип языка - нет такой вещи, как наследование. Но большинство программистов исходят из программирования, который накладывает наследование как "путь". Поэтому мы придумываем всевозможные вещи, чтобы попытаться сделать этот прототип языка "классическим" языком, например, расширением "классов". Действительно, в примере, который они дали, новый студент - это человек - он не "распространяется" от другого ученика. Студент - это все о человеке, и независимо от того, кем является ученик, также. Расширьте учащегося, и все, что вы расширили, - это студент в глубине души, но настроен в соответствии с вашими потребностями.

Крокфорд немного сумасшедший и переусердствующий, но делает некоторые серьезные чтения по некоторым материалам, которые он написал. Это заставит вас взглянуть на этот материал совсем по-другому.

Ответ 4

TL;DR; Не очень необходимо, но, вероятно, поможет в долгосрочной перспективе, и это более точно.

ПРИМЕЧАНИЕ. Многое редактировалось, так как мой предыдущий ответ был смущенно написан и имел некоторые ошибки, которые я пропустил в своей спешке, чтобы ответить. Спасибо тем, кто указал на некоторые вопиющие ошибки.

В принципе, он правильно подключает подклассы в 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 возвращал true и чтобы экземпляры 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 .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 с помощью . Поэтому, если мы назовем 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;
   }
}

Ответ 5

Это огромная ошибка, если вы написали

Student.prototype.constructor = Student;

но тогда, если бы существовал Учитель, чей прототип был также Человеком, и вы написали

Teacher.prototype.constructor = Teacher;

тогда конструктор Стьюдента теперь является Учителем!

Изменить: Вы можете избежать этого, убедившись, что вы установили прототипы Student и Teacher, используя новые экземпляры класса Person, созданные с использованием Object.create, как в примере Mozilla.

Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);

Ответ 6

До сих пор путаница все еще существует.

Следуя исходному примеру, поскольку у вас есть существующий объект student1 as:

var student1 = new Student("Janet", "Applied Physics");

Предположим, вы не хотите знать, как создается student1, вам просто нужен другой объект, как он, вы можете использовать свойство конструктора student1, например:

var student2 = new student1.constructor("Mark", "Object-Oriented JavaScript");

Здесь он не сможет получить свойства из Student, если свойство конструктора не установлено. Скорее он создаст объект Person.

Ответ 7

Получил хороший пример кода, почему действительно необходимо установить конструктор прототипов.

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 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 ???? 
*/

Ответ 8

Не нужно использовать классы sugared function или использовать "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 и С++, являются основанный на концепции двух разных сущностей: классов и экземпляры.

...

Язык на основе прототипа, такой как JavaScript, не делает этого различие: у него просто есть объекты. Язык, основанный на прототипе, имеет понятие прототипического объекта, объект, используемый в качестве шаблона из для получения начальных свойств для нового объекта. Любой объект может укажите его собственные свойства либо при его создании, либо во время выполнения. Кроме того, любой объект может быть связан как прототип для другого объект, позволяя второму объекту совместно использовать первый объект Свойства

Ответ 9

Это необходимо, если вам нужна альтернатива toString без monkeypatching:

//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"]);

Ответ 10

EDIT, я был на самом деле неправ. Комментирование строки не изменяет ее поведение вообще. (Я протестировал его)


Да, это необходимо. Когда вы делаете

Student.prototype = new Person();  

Student.prototype.constructor становится Person. Поэтому вызов Student() возвращает объект, созданный Person. Если вы делаете

Student.prototype.constructor = Student; 

Student.prototype.constructor - reset назад к Student. Теперь, когда вы вызываете Student(), он выполняет Student, который вызывает родительский конструктор Parent(), он возвращает правильно наследуемый объект. Если вы не отправили reset Student.prototype.constructor перед вызовом, вы получили бы объект, у которого не было бы никаких свойств, установленных в Student().

Ответ 11

Это не нужно, если вы используете фабрики вместо конструкторов с прототипным наследованием. Дополнительные преимущества:

  • правильная инкапсуляция частной переменной (вместо того, чтобы подвергать их как this.firstName);
  • удаление необходимости использовать new, который обычно забывается как главный источник ошибок;
  • устранение необходимости использования ссылки this, которая сама по себе также является источником путаницы (которая фактически отвечает за необходимость установки constructor в связанном примере);
  • удаление необходимости ссылаться на object.prototype, который идет в направлении, противоположном естественному наследованию (от прототипа до объекта);
  • последнее, но не менее важное, устраняя необходимость установки constructor, которую Крокфорд считает одной из "плохих частей".

Следующий код демонстрирует, как использовать этот подход, чтобы избежать всех этих проблем и добиться того же результата:


// define factory
var CreatePerson = function(firstName) {

  // revealing pattern
  return {
    walk: walk,
    sayHello: sayHello
  };

  function walk () {
    console.log("I am walking!");    
  }

  // Using the local variable firstName,
  // which is properly encapsulated and inaccessible from outside
  function sayHello () {
    console.log("Hello, I'm " + firstName);
  }
};

// testing the factory - no need for "new"!
var girl = CreatePerson("Jane");
girl.walk();
girl.sayHello();


// now define another factory to create objects with additional methods
var CreateStudent = function(firstName, subject) {
  var person = CreatePerson(firstName);
  var newMethods = {
   sayHello: function(){
      console.log("Hello, I'm " + firstName + ". I'm studying " + subject + "."); 
   },
    sayGoodBye: function() {
      console.log("Goodbye!");
    }    
  };

  // add new methods
  var student = Object.assign(person, newMethods);

  // return the object inheriting from student
  return Object.create(student);
};

// testing the factory
var student = CreateStudent("Janet", "Applied Physics");
student.sayHello();
student.sayGoodBye();


Вот jsbin для воспроизведения с.

Ответ 12

Указанная простая конструкторская функция:

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), все прототипы автоматически получают свойство, называемое конструктором, которое указывает на функцию, на которой она является свойством. В зависимости от конструктора к прототипу могут быть добавлены другие свойства и методы, которые не являются очень распространенной практикой, но все же разрешены для расширений.

Таким образом, просто отвечая: нам нужно убедиться, что значение в prototype.constructor правильно установлено, поскольку предполагается, что спецификация будет.

Нужно ли всегда правильно устанавливать это значение? Это помогает при отладке и делает внутреннюю структуру совместимой со спецификацией. Мы должны определенно, когда наш API будет использоваться третьими сторонами, но не совсем, когда код будет окончательно выполнен во время выполнения.

Ответ 13

Это не обязательно. Это просто одно из многих традиционных, чемпионы ООП, чтобы попытаться превратить прототипное наследование 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 классические наследования пуристы могут использовать языковые родные операторы, такие как класс, extends и super, и нам не нужно видеть, как исправления prototype.constructor и родительские рефери.