关于javascript:为什么有必要设置原型构造函数?

Why is it necessary to set the prototype constructor?

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

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

这有什么重要的用途吗?可以忽略它吗?


它并不总是必要的,但它确实有它的用途。假设我们想在基础Person类上创建一个复制方法。这样地:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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并复制它时会发生什么?

1
2
var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => false

副本不是Student的实例。这是因为(如果没有明确的检查),我们将无法从"base"类返回Student副本。我们只能退回一个Person。但是,如果我们重置了构造函数:

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

…然后一切按预期工作:

1
2
var student1 = new Student("trinth");  
console.log(student1.copy() instanceof Student); // => true


Does this serve any important purpose?

是和不是。

在ES5和更早的版本中,javascript本身没有为任何东西使用constructor。它定义了一个函数的prototype属性上的默认对象将拥有它,并且它将引用该函数,即它。规范中没有提到它。

这在ES2015(ES6)中发生了变化,它开始在继承层次结构中使用它。例如,Promise#then在建立新的返回承诺时,使用您称之为(通过特定的构造者)的承诺的constructor属性。它还涉及到子类型数组(通过arrayspecialsreate)。

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

Is it okay to omit it?

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

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

如果不这样做:

1
Student.prototype.constructor = Student;

…然后,Student.prototype.constructor继承了Person.prototype,据推测,constructor = Person是由Person.prototype继承的。所以这是误导。当然,如果您要对使用它的对象(如PromiseArray进行子类化,而不使用class1(它为您处理这个问题),您需要确保正确设置它。所以基本上:这是个好主意。

如果您的代码(或您使用的库代码)中没有任何东西使用它,也可以。我一直确保它的接线正确。

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

1
2
class Student extends Person {
}

1"…如果你将使用它的东西(如PromiseArray子类化,而不使用class……"是可能的,但这是真正的痛苦(而且有点愚蠢)。你必须使用Reflect.construct


TLDR;不是非常必要,但从长远来看可能会有所帮助,而且这样做更准确。

注:由于我以前的答案被编辑得很混乱,在我急于回答的过程中有一些错误我错过了。感谢那些指出了一些令人震惊的错误的人。

基本上,它是在JavaScript中正确地连接子类。当我们进行子类化时,我们必须做一些奇怪的事情来确保原型委托的工作正常,包括覆盖prototype对象。覆盖prototype对象包括constructor对象,因此我们需要修复引用。

让我们快速了解ES5中的"类"是如何工作的。

假设您有一个构造函数函数及其原型:

1
2
3
4
5
6
7
8
9
10
//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

1
2
// instantiate using the 'new' keyword
var adam = new Person('Adam', 19);

使用"person"调用的new关键字基本上将使用一些附加代码行来运行person构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.prototypePerson.prototype具有.species属性,因此查找将在Person.prototype成功。然后它将记录'human'

在这里,Person.prototype.constructor将正确指向Person

现在有趣的部分,所谓的"子类化"。如果我们想创建一个Student类,它是Person类的一个子类,并且有一些额外的更改,我们需要确保Student.prototype.constructor指向学生以获得准确性。

它本身并不能做到这一点。子类化时,代码如下所示:

1
2
3
4
5
6
7
8
9
10
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返回正确的结果,使Student的实例正确地委托给Student.prototype,然后是Person.prototype

但既然我们用new关键字来调用它,还记得调用添加了什么吗?它将称为Object.create(Student.prototype),这就是我们如何在StudentStudent.prototype之间建立授权关系。注意,现在,Student.prototype是空的。因此,查找.speciesStudent的一个实例将失败,因为它只委托Student.prototype.species属性不存在于Student.prototype上。

当我们把Student.prototype分配给Object.create(Person.prototype)时,Student.prototype本身就委托给Person.prototype,查找eve.species将返回human。我们可能希望它继承自student.prototype和person.prototype。所以我们需要解决所有这些问题。

1
2
3
4
5
6
7
8
9
/* 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。这就是为什么我们需要修复它。

1
2
3
4
5
// 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中,EDOCX1[2]现在构建在我们编写类的方式中——就像在中一样,它在我们声明类时作为一种方法提供。这只是简单的句法糖分,但它确实给了我们一些便利,比如在扩展现有类时访问super。因此,我们可以这样编写上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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;
   }
}


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

1
2
3
4
5
6
7
8
Person = function () {
    this.favoriteColor = 'black';
}

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

在测试代码的末尾…

1
alert(student1.favoriteColor);

颜色将是蓝色。

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

编辑:在浏览了一下Web并进行了一些实验之后,人们似乎设置了构造函数,使其"看起来"像是用"new"构建的东西。我想我会争辩说,这方面的问题是,javascript是一种原型语言——没有继承这样的东西。但是大多数程序员都来自于将继承性作为"方式"来推动的编程背景。所以我们想出了各种各样的办法,试图使这种原型语言成为一种"经典"语言。例如扩展"类"。实际上,在他们给出的例子中,一个新学生就是一个人——它不是从另一个学生那里"延伸"出来的。学生是关于人的,不管人是什么,学生也是。扩展学生,不管你扩展了什么,都是一个学生,但它是根据你的需要定制的。

克罗克福德有点疯狂和过分热情,但认真阅读他写的一些东西。这会让你对这些东西的看法大相径庭。


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

1
Student.prototype.constructor = Student;

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

1
Teacher.prototype.constructor = Teacher;

那么学生建造师现在是老师了!

编辑:您可以通过确保使用使用object.create创建的Person类的新实例(如Mozilla示例中所示)设置学生和教师原型来避免这种情况。

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


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

按照原始示例,由于现有对象student1为:

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

假设您不想知道student1是如何创建的,您只需要一个类似它的对象,就可以使用student1的constructor属性,比如:

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

如果没有设置constructor属性,它将无法从Student中获取属性。相反,它将创建一个Person对象。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 ????
*/


它是必要的,当你需要一个新的toStringmonkeypatching:没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//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"]);


现在不需要加糖函数"classes"或使用"new"。使用对象文本。

对象原型已经是一个"类"。定义对象文字时,它已经是原型对象的实例。它们也可以作为另一个对象的原型等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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]

值得一读:

Class-based object-oriented languages, such as Java and C++, are
founded on the concept of two distinct entities: classes and
instances.

...

A prototype-based language, such as JavaScript, does not make this
distinction: it simply has objects. A prototype-based language has the
notion of a prototypical object, an object used as a template from
which to get the initial properties for a new object. Any object can
specify its own properties, either when you create it or at run time.
In addition, any object can be associated as the prototype for another
object, allowing the second object to share the first object's
properties


这里是一个例子,我发现非常有用的MDN明白其使用。

在JavaScript中,我们async functions这asyncfunction对象返回。AsyncFunction不是一个全局对象,但它可以检索通过使用一个constructor财产和使用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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(){...}

默认(从规范developer.mozilla.org http:///EN /文档/网络/美国/全球_ JavaScript参考/对象/对象/构造函数),都称为构造函数的原型自动得到一点回,物业的功能上,它是一个物业。取决于其他的构造函数,属性和方法可以被添加到的原型是一个普通的实践困境,但仍然是允许的扩展。

所以简单的回答:我们需要确保的是,在一个正确的价值prototype.constructor集,它是由不规范的。

我们总是做正确设置这个值?它可以帮助在调试和内部结构与规范一致。我们应该肯定的,当我们使用的API是由thirdparties,但不是真的当是最后执行的代码在运行时。


编辑,我错了。评论这一行根本不会改变它的行为。(我测试过)

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

1
Student.prototype = new Person();

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

1
Student.prototype.constructor = Student;

Student.prototype.constructor复位回Student。现在,当调用Student()时,它执行Student,它调用父构造函数Parent(),它返回正确继承的对象。如果在调用之前没有重置Student.prototype.constructor,您将得到一个对象,该对象不具有Student()中设置的任何属性。


这是不必要的。它是只是一个传统的OOP做许多的事情,尝试体操冠军prototypical JavaScript的继承和继承的。唯一的东西,

1
Student.prototype.constructor = Student;

是,是,你现在有一个参考的电流"建设者"。

在韦恩已经标记为答案,这是正确的,你可以在同一确切的东西下面的代码是

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

下面的代码是一个传统的人this.constructor)

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

感谢上帝,在继承古典es6 purists CAN使用语言的本地运营商扩展类和超类,我们不prototype.constructor湖样校正和家长参考。