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

时间:2011-12-10 02:12:10

标签: javascript oop inheritance

section about inheritance in the MDN article Introduction to Object Oriented Javascript中,我注意到他们设置了prototype.constructor:

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

这有什么重要意义吗?省略它可以吗?

13 个答案:

答案 0 :(得分:244)

并不总是必要的,但确实有它的用途。假设我们想在基类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的实例。这是因为(没有明确的检查),我们无法从“base”类返回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

答案 1 :(得分:11)

<强> 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);

使用'Person'调用的new关键字基本上将使用一些额外的代码行运行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 - 和{{1 具有 Person.prototype属性,因此查找将在.species处成功。然后它会记录Person.prototype

此处,'human'会正确指向Person.prototype.constructor

所以现在是有趣的部分,即所谓的“子类化”。如果我们要创建一个Person类,这是Student类的子类并进行一些其他更改,我们需要确保Person指向Student以确保准确性。

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

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返回true,以便eve instanceof Person的实例正确委托给Student,然后Student.prototype

但是因为我们使用Person.prototype关键字调用它,还记得调用添加了什么吗?它会调用new,这就是我们在Object.create(Student.prototype)Student之间设置委托关系的方式。请注意,目前Student.prototype为空。因此查找Student.prototype .species实例会失败,因为它仅委派 StudentStudent.prototype属性不存在于.species {1}}。

当我们将Student.prototype分配给Student.prototypeObject.create(Person.prototype)本身然后委托给Student.prototype,查找Person.prototype将返回eve.species as我们期待。据推测,我们希望它继承自Student.prototype和Person.prototype。所以我们需要解决所有问题。

human

现在代表团工作,但我们用/* 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

在ES5中,我们的// 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 属性是一个引用,它引用了我们编写的函数,意图是'构造函数'。除了constructor关键字给我们的内容之外,构造函数还是一个“普通”函数。

在ES6中,new现在构建在我们编写类的方式中 - 就像在我们声明一个类时它作为一个方法提供的。这只是语法糖,但它确实给了我们一些便利,例如当我们扩展现有类时访问constructor。所以我们会像这样编写上面的代码:

super

答案 2 :(得分:10)

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

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

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

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

alert(student1.favoriteColor);

颜色为蓝色。

根据我的经验,对prototype.constructor的更改没有太大作用,除非你做的非常具体,非常复杂的事情可能不是很好的做法:)

编辑: 在网上浏览了一下并进行了一些实验之后,看起来人们设置了构造函数,使它看起来像是用'new'构造的东西。我想我会争辩说这个问题是javascript是一种原型语言 - 没有继承权这样的东西。但大多数程序员都来自编程的背景,这种编程将继承作为“方式”。因此,我们想出各种各样的东西来尝试将这种原型语言变成“经典”语言......例如扩展“类”。实际上,在他们给出的例子中,一个新学生就是一个人 - 它不是从另一个学生那里“延伸”......学生就是关于这个人的,而且无论学生是谁,也是如此。扩展学生,无论您扩展什么,都是学生的核心,但可以根据您的需求进行定制。

Crockford有点疯狂和过分热心,但对他写的一些东西做了一些认真的阅读......它会让你以非常不同的方式看待这些东西。

答案 3 :(得分: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);

答案 4 :(得分:5)

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

按照原始示例,因为您有一个现有对象student1

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

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

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

如果未设置构造函数属性,则无法从Student获取属性。相反,它会创建一个Person对象。

答案 5 :(得分: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 ???? 
*/

答案 6 :(得分:1)

这些天不需要加糖功能'类'或使用'新'。使用对象文字。

Object原型已经是一个'类'。定义对象文字时,它已经是原型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]

This is worth a read

  

基于类的面向对象语言,例如Java和C ++   基于两个不同实体的概念:类和   实例

     

...

     

基于原型的语言(例如JavaScript)不能实现这一点   区别:它只是有对象。基于原型的语言有   原型对象的概念,用作模板的对象   哪个获取新对象的初始属性。任何对象都可以   在创建它时或在运行时指定它自己的属性。   此外,任何对象都可以作为另一个对象的原型关联   对象,允许第二个对象共享第一个对象   特性

答案 7 :(得分: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"]);

答案 8 :(得分:0)

编辑,我其实是错的。评论该线路根本不会改变它的行为。 (我测试了它)

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

Student.prototype = new Person();  

Student.prototype.constructor变为Person。因此,调用Student()将返回由Person创建的对象。如果你那么做

Student.prototype.constructor = Student; 

Student.prototype.constructor重置为Student。现在当你调用Student()它执行调用父构造函数Student的{​​{1}}时,它会返回正确继承的对象。如果您在调用之前没有重置Parent(),那么您将获得一个不会在Student.prototype.constructor中设置任何属性的对象。

答案 9 :(得分: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),所有原型都会自动获得一个名为constructor的属性,该属性指向它所属的函数。 根据构造函数的不同,可能会在原型中添加其他属性和方法,这不是一种非常常见的做法,但仍允许进行扩展。

所以简单回答:我们需要确保prototype.constructor中的值被正确设置,因为规范应该是这样。

我们是否必须始终正确设置此值?它有助于调试并使内部结构与规范保持一致。当我们的API被第三方使用时,我们当然应该这样,但是当代码最终在运行时中执行时,我们就不应该这样做。

答案 10 :(得分: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
});

答案 11 :(得分:0)

这是必要的。类继承中的任何类都必须具有自己的构造函数,例如原型继承中那样,这也便于对象构造。但是这个问题是不必要的,需要了解在JavaScript世界中调用函数作为构造函数的作用以及解决对象属性的规则。

以表达式 新<函数名>([参数])

作为构造函数执行函数的效果
  1. 创建一个类型名称为函数名称的对象
  2. 函数中的
  3. 内部属性会附加到创建的对象上
  4. 函数的
  5. 属性 原型 作为原型自动附加到所创建的对象

对象解析属性的规则

  • 不仅将在对象上寻求该属性,还将在对象的原型,原型的原型上等等,直到找到具有匹配名称的属性或到达原型链的末尾为止。

基于这些基础机制,语句 .prototype.constructor = 在效果上等于用表达式将构造函数附加到构造函数主体中 this.constructor = <构造函数名称> 。如果是第二种话语,则构造函数将在对象上解析;如果是第二种话语,则将在对象的原型上解析。

答案 12 :(得分:-1)

没有必要。这只是传统的OOP冠军之一,试图将JavaScript的原型继承转化为经典继承。以下唯一的事情

Student.prototype.constructor = Student; 

是,你现在有一个当前&#34;构造函数的引用&#34;。

在Wayne的回答中,已经标记为正确,你可能与以下代码完全相同

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经典继承纯粹主义者可以使用语言的本地运算符,如类,扩展和超级,我们不必看到像prototype.constructor更正和父级引用。