为什么需要设置原型构造函数?

IT技术 javascript oop inheritance
2021-01-15 15:44:40

MDN 文章面向对象的 Javascript 简介中关于继承部分中,我注意到他们设置了prototype.constructor:

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

这有什么重要目的吗?省略它可以吗?

6个回答

它并不总是必要的,但它确实有它的用途。假设我们想在基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
我想我明白了。但是,如果有什么Student构造又增加了像一个额外的参数:Student(name, id)然后我们是否必须覆盖该copy函数,Person从其中调用版本,然后还复制附加id属性?
2021-03-17 15:44:40
@Pumbaa80 - 我明白你的意思,但引擎自动初始化的事实constructor意味着它在 JS 中确实具有特殊含义,几乎根据定义。
2021-03-20 15:44:40
@lwburk“//同样糟糕”是什么意思?
2021-03-20 15:44:40
注意:该constructor属性在JS中并没有什么特殊的含义,不妨称之为bananashake唯一的区别是发动机自动初始化constructorf.prototype时,你声明的功能f但是,它可以随时被覆盖。
2021-03-22 15:44:40
我只是想澄清一下,您所说的行为有效的原因是因为您使用return new this.constructor(this.name);return new Person(this.name);. 由于this.constructorStudent函数(因为您用 设置了它Student.prototype.constructor = Student;),copy函数最终会调用该Student函数。我不确定你的意图是什么//just as bad评论。
2021-03-23 15:44:40

这有什么重要目的吗?

是和否。

在 ES5 和更早的版本中,JavaScript 本身没有任何用途constructor它定义了函数prototype属性上的默认对象将拥有它,并且它将引用回函数,就是这样规范中没有任何其他内容提及它。

这在 ES2015 (ES6) 中发生了变化,它开始在继承层次结构中使用它。例如,在构建要返回的新Promise时Promise#then使用constructor您调用它的Promise属性(通过SpeciesConstructor)。它还涉及子类型数组(通过ArraySpeciesCreate)。

在语言本身之外,有时人们会在尝试构建通用的“克隆”函数时使用它,或者只是在他们想要引用他们认为是对象的构造函数时一般会使用它。我的经验是很少使用它,但有时人们确实会使用它。

省略它可以吗?

默认情况下它就在那里,您只需要在替换函数prototype属性上的对象时将其放回原处

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

如果你不这样做:

Student.prototype.constructor = Student;

...然后Student.prototype.constructor继承Person.prototype其中(大概)有constructor = Person. 所以这是误导。当然,如果您要对使用它的东西(如PromiseArray)进行子类化而不使用class¹(它会为您处理此问题),您需要确保正确设置它。所以基本上:这是一个好主意。

如果您的代码(或您使用的库代码)中没有任何内容使用它,那也没关系。我一直确保它连接正确。

当然,使用 ES2015(又名 ES6)的class关键字,大多数时候我们会使用它,我们不必再使用它了,因为它会在我们使用时为我们处理

class Student extends Person {
}

¹ “...如果您要对使用它的东西(如PromiseArray)进行子类化而不使用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指向 Student 的准确性。

它本身不会这样做。子类化时,代码如下所示:

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.specieshuman如我们预期的那样返回大概我们希望它继承自 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在我们扩展现有类时访问 a 所以我们会像这样写上面的代码:

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;
   }
}
我的错。它应该读取 'eve instanceof Person' 这将返回 false。我会修改那部分。你是对的,每个函数都有一个原型属性。但是,如果没有将原型分配给Object.create(Person.prototype)Student.prototype则为空。因此,如果我们记录eve.species,它不会正确地委托给它的超类,Person,它也不会记录'human'据推测,我们希望每个子类都继承其原型以及其超类的原型。
2021-03-27 15:44:40
为了澄清,which is, at the moment, nothing我的意思是该Student.prototype对象是空的。
2021-04-01 15:44:40
这里似乎有很多问题:“当您尝试模拟‘子类化’[...] 时,这是必要的,以便当您检查实例是否instance为‘子类’构造函数时,它会是准确的。” 不,instanceof不使用constructor. “但是,如果我们查找学生的 .prototype.constructor,它仍然会指向 Person”不,它会是Student. 我不明白这个例子的意义。在构造函数中调用函数不是继承。“在 ES6 中,构造函数现在是一个实际的函数,而不是对函数的引用”呃什么?
2021-04-01 15:44:40
eve instanceof Student回来了true请参阅stackoverflow.com/questions/35537995/...了解说明。还有当你说which is, at the moment, nothing你指的是什么?每个函数都有一个原型,所以如果我检查Student.prototype它是什么。
2021-04-02 15:44:40
关于原型的更多信息:没有对Student.prototypeto的分配Object.create(Person.prototype)——如果你还记得,这与将 Person 的所有实例设置为委托 to 的方式相同Person.prototype——在 的实例上查找属性Student 委托给Student.prototype所以eve.species查找会失败。如果我们确实分配了它,Student.prototype那么本身将委托给Person.prototype,查找eve.species将返回human
2021-04-02 15:44:40

我不同意。没有必要设置原型。使用完全相同的代码,但删除prototype.constructor 行。有什么变化吗?不。现在,进行以下更改:

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

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

并在测试代码的末尾......

alert(student1.favoriteColor);

颜色将是蓝色。

根据我的经验,对prototype.constructor 的更改并没有太大作用,除非您正在做非常具体、非常复杂的事情,而这些事情无论如何都可能不是很好的做法:)

编辑:在网上闲逛了一下并进行了一些实验后,看起来人们设置了构造函数,使其“看起来”像用“新”构造的东西。我想我会争辩说,问题在于 javascript 是一种原型语言 - 没有继承这样的东西。但是大多数程序员都具有将继承作为“方式”的编程背景。所以我们想出了各种各样的方法来尝试使这种原型语言成为“经典”语言……例如扩展“类”。真的,在他们给出的例子中,一个新学生是一个人——它不是从另一个学生“延伸”出来的……学生就是这个人,无论这个人是什么,学生也是如此。扩展学生,不管你

克罗克福德有点疯狂和过分热情,但认真阅读他写的一些东西......它会让你对这些东西有非常不同的看法。

这不继承原型链。
2021-03-14 15:44:40
@Cypher 代码片段基于链接文章中的代码。欢迎阅读完整的问题。哦。等待。
2021-03-29 15:44:40
您缺少继承原型的代码。欢迎来到互联网。
2021-04-01 15:44:40
@Cypher慢拍欢迎四年后的谈话。是的,原型链继承的,不管你是否覆盖了prototype.constructor。尝试测试一下。
2021-04-08 15:44:40
@macher 我的意思是它是经典继承。我的措辞选择不当。
2021-04-09 15:44:40

这有一个巨大的陷阱,如果你写

Student.prototype.constructor = Student;

但是如果有一个老师的原型也是 Person 而你写的

Teacher.prototype.constructor = Teacher;

那么学生构造函数现在是老师!

编辑:您可以通过确保使用 Object.create 创建的 Person 类的新实例设置 Student 和 Teacher 原型来避免这种情况,如 Mozilla 示例中所示。

Student.prototype = Object.create(Person.prototype);
Teacher.prototype = Object.create(Person.prototype);
更广泛的一点是,有一些陷阱会吸引那些不熟悉 Javascript 原型的人。如果我们在 2016 年讨论,那么你真的应该使用 ES6 类、Babel 和/或 Typescript。但是,如果您真的想以这种方式手动构造类,那么了解原型链如何真正发挥作用以利用其功能会有所帮助。您可以使用任何对象作为原型,也许您不想新建一个单独的对象。此外,在 HTML 5 完全普及之前,Object.create 并不总是可用,因此更容易错误地设置类。
2021-03-11 15:44:40
问题中引用的链接文章已经使用 Object.create()。这个答案和答案的编辑并不真正相关,至少可以说令人困惑:-)
2021-03-15 15:44:40
Student.prototype = Object.create(...)在这个问题中假设。这个答案只会增加可能的混乱。
2021-03-16 15:44:40
@AndréNeves 我发现这个答案很有帮助。Object.create(...)用于产生该问题的 MDN 文章中,但未用于问题本身。相信很多人都不会点进去。
2021-03-26 15:44:40