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


294

MDN文章“面向对象的Java语言简介”中有关继承部分中,我注意到它们设置了prototype.constructor:

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

这有任何重要目的吗?可以省略吗?


23
很高兴您这样问:我昨天看了同一篇文档,并对显式设置构造函数背后的原因感到好奇。
Wylie

6
我只需要指出这一点,这个问题现在就链接到您链接的文章中了!
2015年

7
没有什么必要了
nothingisnecessary

1
subclass.prototype.constructor会指出parent_class,如果你不写subclass.prototype.constructor = subclass; 也就是说,subclass.prototype.constructor()直接使用会产生意想不到的结果。
关羽初

Answers:


263

它并不总是必需的,但是它确实有其用途。假设我们想在基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

34
注意:该constructor属性在JS中没有任何特殊含义,因此您最好将其称为bananashake。唯一的区别是发动机自动初始化constructorf.prototype时,你声明的功能f。但是,它可以随时被覆盖。
user123444555621 2011年

58
@ Pumbaa80-我明白了,但是引擎自动初始化的事实constructor意味着它在JS中确实具有特殊的含义,从定义上讲几乎是这样。
韦恩2012年

13
我只想澄清一下,您说的行为之所以有效,是因为您使用return new this.constructor(this.name);而不是return new Person(this.name);。由于this.constructorStudent函数(因为您使用进行了设置Student.prototype.constructor = Student;),因此该copy函数最终调用了该Student函数。我不确定您的意图是什么//just as bad
CEGRD

12
@lwburk“ //同样糟糕”是什么意思?
CEGRD

6
我想我明白了。但是,如果Student构造函数添加了额外的参数,例如:Student(name, id)呢?然后,我们是否必须重写该copy函数,Person从其中调用版本,然后再复制其他id属性?
snapfractalpop

76

这有任何重要目的吗?

是的,没有。

在ES5和更早的版本中,JavaScript本身不使用constructor任何东西。它定义了函数prototype属性上的默认对象将拥有它,并且它将引用回该函数,仅此而已。规范中没有其他内容。

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

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

可以省略吗?

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

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

如果您不这样做:

Student.prototype.constructor = Student;

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

如果您的代码(或您使用的库代码)中没有任何内容,则可以使用。我一直确保它已正确连接。

当然,有了ES2015(aka ES6)的class关键字,大多数时候我们会使用它,我们不再需要了,因为当我们这样做时,它会为我们处理

class Student extends Person {
}

¹ “ ...如果您将使用它的东西(例如PromiseArray)子类化而没有使用class...”  – 可以这样做,但这确实很痛苦(有点傻)。您必须使用Reflect.construct


12

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.species将按human我们期望的那样返回。大概我们希望它继承自Student.prototype和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.prototypePerson.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 Student返回true。有关说明,请参见stackoverflow.com/questions/35537995/…。另外,当您说which is, at the moment, nothing什么时您指的是什么?每个函数都有一个原型,所以如果我检查Student.prototype它的话。
Aseem Bansal

我的错。它应该读为“ eve instanceof Person”,它将返回false。我将修改该部分。您是正确的,每个函数都有一个原型属性。但是,如果不将原型分配给Object.create(Person.prototype)Student.prototype则为空。因此,如果我们登录eve.species,它将无法正确地委派给其超类Person,并且也将不会登录'human'。大概,我们希望每个子类都继承自其原型以及其父级的原型。
bthehuman

通过澄清,which is, at the moment, nothing我的意思是该Student.prototype对象为空。
bthehuman

更多关于原型:如果没有分配Student.prototypeObject.create(Person.prototype)-这是,如果你还记得,以同样的方式人的所有情况下都设置为委托Person.prototype-上的一个实例查找属性Student将委托 Student.prototype。因此eve.species将无法进行查找。如果我们确实分配了它,Student.prototype则它本身将委托给Person.prototype,并且查找eve.species将返回human
bthehuman

似乎有很多错误在这里:“当您试图模仿'子类'[...]时,这是必要的,以便当您检查实例是否instance为'子类'构造函数时,它将是准确的。” 不,instanceof不使用constructor“但是,如果我们查找学生的.prototype.constructor,它仍将指向Person。”不,它将是Student。我不明白这个例子的意义。在构造函数中调用函数不是继承。“在ES6中,构造函数现在是实际函数,而不是对函数的引用”嗯?
Felix Kling

10

我不同意。无需设置原型。采取完全相同的代码,但删除prototype.constructor行。有什么变化吗?否。现在,进行以下更改:

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

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

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

alert(student1.favoriteColor);

颜色将为蓝色。

根据我的经验,对prototype.constructor的更改不会做很多事情,除非您要进行非常具体,非常复杂的操作,但这些操作可能都不是一个好习惯:)

编辑:在网络上闲逛了一段时间并做了一些实验之后,看起来人们已经设置了构造函数,使其“看起来”就像正在使用“ new”构造的事物。我想我会争论这的问题是javascript是一种原型语言-没有继承性。但是大多数程序员都来自将继承视为“方式”的编程背景。因此,我们想出了各种方法来尝试使这种原型语言成为一种“经典”语言,例如扩展“类”。确实,在他们给出的示例中,一个新学生是一个人-它不是从另一个学生“扩展”过来的。该学生就是这个人的全部,无论这个人是哪个学生,也是如此。扩展学生,无论您是什么

克罗克福德(Crockford)有点疯狂和狂热,但是请认真阅读他所写的一些东西。这会让您对这些东西的看法大为不同。


8
这不会继承原型链。
Cypher

1
@Cypher 慢拍四年后,欢迎大家来交谈。是的,无论是否覆盖prototype.constructor ,原型链都是继承的。尝试测试一下。
斯蒂芬

7
您缺少继承原型的代码。欢迎使用互联网。
Cypher

1
@Cypher代码段基于链接文章中的代码。欢迎阅读完整的问题。哦。等待。
斯蒂芬

1
@macher我把它当作经典继承。我的措词选择不当。
斯蒂芬

9

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

Student.prototype.constructor = Student;

但是如果有一位老师的原型也是人,而你写了

Teacher.prototype.constructor = Teacher;

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

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

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

1
Student.prototype = Object.create(...)假设在这个问题上。这个答案只会增加混乱。
安德烈(AndréChalella)

3
@AndréNeves我发现这个答案很有帮助。Object.create(...)在产生问题的MDN文章中使用,而不是在问题本身中使用。我敢肯定很多人不会点击。
亚历克斯·罗斯

问题清单中引用的链接文章使用Object.create()。这个答案和“编辑该答案”并没有真正的意义,至少可以说
一头雾水

1
更广泛的观点是,有些陷阱会吸引Java原型新手。如果我们在2016年讨论,那么您应该真正使用ES6类,Babel和/或Typescript。但是,如果您真的想以这种方式手动构造类,则有助于理解原型链如何真正发挥作用以发挥其功能。您可以将任何对象用作原型,也许您不想新建一个单独的对象。此外,在HTML 5尚未完全普及之前,Object.create并不总是可用,因此更容易错误地设置类。
James D

5

到目前为止,混乱仍然存在。

遵循原始示例,因为您已有一个对象,student1如下所示:

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

假设您不想知道如何 student1创建,只想要另一个对象,可以使用student1like 的构造器属性:

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

Student如果未设置构造函数属性,将无法从中获取属性。而是将创建一个Person对象。


2

有一个很好的代码示例,说明了为什么真的有必要设置原型构造函数。

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

您的createNewCar方法是建立工厂!同样,这看起来应该使用它var audiFactory = new CarFactory("Audi")而不是使用继承。
Bergi 2015年

您的示例在this.constructor内部使用,因此设置它并不奇怪。没有它,您有什么例子吗?
Dmitri Zaitsev

1

如今,不需要加糖的函数“类”或使用“新建”。使用对象文字。

对象原型已经是一个“类”。定义对象文字时,它已经是原型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)没有这种区别:它只是具有对象。基于原型的语言具有原型对象的概念,该对象用作模板,从模板获取新对象的初始属性。任何对象都可以在创建对象时或在运行时指定其自己的属性。此外,任何对象都可以关联为另一个对象的原型,从而允许第二个对象共享第一个对象的属性


1

当您需要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"]);


这应该做什么?foo.constructor()??
Ry-

0

编辑,我实际上是错的。注释掉行根本不会改变它的行为。(我测试过)


是的,这是必要的。当你做

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()


3
原型结构可以成为一个人,但这是适当的,因为它继承了该人的所有属性和方法。在不设置prototype.constructor的情况下创建新的Student()会适当地调用其自己的构造函数。
斯蒂芬

0

给定简单的构造函数:

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时使用,而不是在运行时最终执行代码时才使用。


0

这是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
});

-1

没有必要。这只是传统上的许多事情,OOP的拥护者试图将JavaScript的原型继承转换为经典继承。唯一的以下

Student.prototype.constructor = Student; 

确实,您现在有了当前“构造函数”的引用。

在韦恩的答案中,该答案已被标记为正确,您可以执行与以下代码完全相同的操作

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new this.constructor(this.name);
};  

用下面的代码(只需用Person替换this.constructor)

Person.prototype.copy = function() {  
    // return new Person(this.name); // just as bad
    return new Person(this.name);
}; 

感谢上帝,有了ES6,经典继承者就可以使用语言的本机运算符,例如class,extends和super,而我们不必像prototype.constructor更正和父代引用一样。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.