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

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

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

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

61414 次浏览

编辑,其实我错了。注释掉一行并不会改变它的行为。(我测试过)


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

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()中设置的任何属性的对象。

我不同意。不需要设置原型。取完全相同的代码,但去掉原型。构造函数。有什么变化吗?不。现在,进行以下更改:

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


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

在测试代码的末尾……

alert(student1.favoriteColor);

颜色是蓝色的。

对原型的更改。构造函数,根据我的经验,没有做太多,除非你做非常具体,非常复杂的事情,这可能不是一个好的实践:)

< p >编辑: 在网上闲逛了一会儿,做了一些实验后,看起来人们设置了构造函数,使它看起来像用“new”构造的东西。我想我要说的问题是javascript是一种原型语言——不存在所谓的继承。但大多数程序员的编程背景都将继承作为一种“方式”。所以我们想出了各种各样的办法,试图把这种原型语言变成一种“经典”语言。比如扩展“类”。实际上,在他们给出的例子中,一个新学生是一个人——它不是从另一个学生“延伸”出来的。学生的一切都是关于人的,无论人是什么,学生也是。扩展学生,无论您扩展的是什么,本质上都是学生,但都是根据您的需要定制的。< / p >

克罗克福德有点疯狂和过分热心,但认真阅读他写的一些东西。它会让你从不同的角度看待这个问题。

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

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

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

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

Student.prototype.constructor = Student;

但如果有一个老师,他的原型也是人,你写

Teacher.prototype.constructor = Teacher;

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

< p >编辑: 您可以通过确保您已经使用使用Object创建的Person类的新实例设置了Student和Teacher原型来避免这种情况。

.
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对象。

这有什么重要的目的吗?

是也不是。

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

这种情况在ES2015 (ES6)中改变了,它开始在继承层次结构中使用它。例如,在构建要返回的新promise时,Promise#then使用你所调用的promise的constructor属性(通过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 {
}

¹"...如果你子类化了一些使用它的东西(比如__ABC0或__ABC1),而没有使用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);

使用'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 -和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),这就是我们在StudentStudent.prototype之间建立委托关系的方式。注意,现在Student.prototype是空的。因此,在.species中查找Student的实例将会失败,因为它将委托给Object.create(Student.prototype)0 Student.prototype,并且.species属性在Student.prototype上不存在。

当我们确实将Student.prototype赋值给Object.create(Person.prototype)时,Student.prototype本身将委托给Person.prototype,并且查找eve.species将如我们预期的那样返回human。假设我们希望它从Student继承。原型和人。所以我们需要解决这些问题。

/* 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);

现在委托可以工作了,但是我们用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;
}
}

现在不需要加糖的函数'类'或使用'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++ 建立在两个不同实体的概念上:类和 实例。< / p >

...

基于原型的语言,如JavaScript,不会做到这一点 区别:它只有对象。基于原型的语言具有 原型对象的概念,一个被用作模板的对象 获取新对象的初始属性。任何对象都可以 在创建时或在运行时指定其自己的属性。 此外,任何对象都可以关联为另一个对象的原型 对象,允许第二个对象共享第一个对象的 < / p >属性

给定简单构造函数:

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的属性,该属性指向该属性所在的函数。 根据构造函数的不同,可能会将其他属性和方法添加到原型中,这不是很常见的做法,但仍然允许进行扩展。< / p >

所以简单地回答:我们需要确保原型中的值。构造函数被正确地设置为规范所假定的样子。

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

当你需要一个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"]);

这是不必要的。这只是传统的OOP拥护者所做的许多事情之一,他们试图将JavaScript的原型继承转变为经典继承。唯一的事情是,下面

Student.prototype.constructor = Student;

你现在有了当前“构造函数”的引用。

在Wayne的答案中,它被标记为正确,你可以做与下面代码完全相同的事情

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

使用下面的代码(只需替换此代码。构造师与Person)

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

感谢上帝,在ES6中,经典继承主义者可以使用语言的原生操作符,如class、extends和super,我们不必看到prototype。构造函数更正和父引用。

下面是MDN的一个例子,我发现它对理解它的用法很有帮助。

在JavaScript中,我们有返回AsyncFunction对象的async functionsAsyncFunction不是全局对象,但可以通过使用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世界中调用函数作为构造函数的效果和解析对象属性的规则。

new <函数名>([parameters]) . ([parameters]表达式作为构造函数执行函数的效果

  1. 创建类型名称为函数名的对象
  2. 函数中的内部属性附加到创建的对象
  3. 函数的<强> < /原型强>属性自动附加到创建的对象作为原型

对象属性解析规则

  • 不仅要在对象上查找属性,还要在对象的原型、原型的原型上查找属性,以此类推,直到找到具有匹配名称的属性或到达原型链的末尾。

基于这些底层机制,语句< >强& lt;构造函数name> .prototype。构造函数= <构造函数名称>相当于在构造函数体中附加表达式< >强。构造函数= <构造函数名称>的构造函数。如果第二次发声,构造函数将在对象上解析;如果第一次发声,构造函数将在对象的原型上解析。