为啥要设置原型构造函数?

Posted

技术标签:

【中文标题】为啥要设置原型构造函数?【英文标题】:Why is it necessary to set the prototype constructor?为什么要设置原型构造函数? 【发布时间】:2012-01-17 05:47:12 【问题描述】:

在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;  

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

【问题讨论】:

很高兴你问这个问题:我昨天阅读了相同的文档,并对显式设置构造函数背后的原因感到好奇。 我只需要指出这一点,这个问题现在链接在你链接的文章中! 没有必要 如果你不写subclass.prototype.constructor = subclasssubclass.prototype.constructor 将指向parent_class;即直接使用subclass.prototype.constructor()会产生意想不到的结果。 @KuanYuChu 什么样的unexpected result?我真的很想知道。 【参考方案1】:

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

【讨论】:

注意:constructor 属性在 JS 中没有任何特殊含义,所以你不妨称它为bananashake。唯一的区别是,只要您声明函数f,引擎就会自动在f.prototype 上初始化constructor。但是,它可以随时被覆盖。 @Pumbaa80 - 我明白你的意思,但引擎自动初始化constructor 的事实意味着它确实在 JS 中具有特殊含义,几乎按照定义。 我只是想澄清一下,您所说的行为之所以有效,是因为您使用return new this.constructor(this.name); 而不是return new Person(this.name);。由于this.constructorStudent 函数(因为您使用Student.prototype.constructor = Student; 设置它),所以copy 函数最终会调用Student 函数。我不确定您对 //just as bad 评论的意图是什么。 @lwburk 你说的“//一样糟糕”是什么意思? 我想我明白了。但是,如果Student 构造函数添加了一个额外的参数,例如:Student(name, id),会怎样?然后我们是否必须重写copy 函数,从其中调用Person 版本,然后还要复制附加的id 属性?【参考方案2】:

这有什么重要目的吗?

是和不是。

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

这在 ES2015 (ES6) 中发生了变化,它开始在继承层次结构中使用它。例如,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(又名 ES6)的 class 关键字,大多数时候我们会使用它,我们不必再使用它了,因为它会在我们使用时为我们处理

class Student extends Person 


¹ “...如果您将使用它的东西(例如 PromiseArray)子类化而不使用 class...” - 这是可能的 这样做,但这真的很痛苦(而且有点傻)。你必须使用Reflect.construct

【讨论】:

【参考方案3】:

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 的实例将失败,因为它委托给Student.prototype,并且.species 属性在Student.prototype 上不存在。

当我们将Student.prototype 分配给Object.create(Person.prototype) 时,Student.prototype 本身然后委托给Person.prototype,查找eve.species 将返回human,如我们所料。大概我们希望它继承自 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);

现在委派工作了,但我们用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;
   

【讨论】:

eve instanceof Student 返回true。有关说明,请参阅***.com/questions/35537995/…。另外,当您说which is, at the moment, nothing 时,您指的是什么?每个函数都有一个原型,所以如果我检查 Student.prototype 它就是一些东西。 我的错误。它应该已经读取了返回 false 的“eve instanceof Person”。我会修改那部分。你是对的,每个函数都有一个原型属性。 然而,如果没有将原型分配给Object.create(Person.prototype)Student.prototype 是空的。所以如果我们记录eve.species,它不会正确地委托给它的超类Person,它也不会记录'human'。据推测,我们希望每个子类都继承其原型以及其父类的原型。 澄清一下,which is, at the moment, nothing,我的意思是 Student.prototype 对象是空的。 关于原型的更多信息:没有将Student.prototype 分配给Object.create(Person.prototype) - 如果您还记得的话,就像所有 Person 实例都设置为委托给 Person.prototype 一样 - 寻找在Student 的实例上添加一个属性将委托给only Student.prototype。所以eve.species 将无法查找。如果我们分配它,Student.prototype 本身然后委托给Person.prototype,查找eve.species 将返回human 这里似乎有很多问题:“当您尝试模拟“子类化”时,这是必要的 [...] 这样当您检查实例是否为 @ 987654401@ '子类' 构造函数,它将是准确的。” 不,instanceof 不使用 constructor“但是,如果我们查找学生的 .prototype.constructor,它仍然指向 Person” 不,它将是 Student。我不明白这个例子的意义。在构造函数中调用函数不是继承。 “在 ES6 中,构造函数现在是一个实际的函数,而不是对函数的引用” 呃什么?【参考方案4】:

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

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


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

在测试代码的最后...

alert(student1.favoriteColor);

颜色为蓝色。

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

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

Crockford 有点疯狂和过分热心,但请认真阅读他写的一些东西。这会让你对这些东西的看法大不相同。

【讨论】:

这不继承原型链。 @Cypher slow clap 欢迎来到四年后的对话。是的,原型链继承的,不管你是否覆盖prototype.constructor。尝试测试一下。 您缺少继承原型的代码。欢迎来到互联网。 @Cypher Code sn-p 基于链接文章中的代码。欢迎阅读完整的问题。哦。等等。 @macher 我的意思是经典继承。我的措辞选择不当。【参考方案5】:

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

Student.prototype.constructor = Student;

但是如果有一个教师的原型也是 Person 并且你写了

Teacher.prototype.constructor = Teacher;

那么 Student 构造函数现在是 Teacher!

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

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

【讨论】:

Student.prototype = Object.create(...) 在这个问题中被假定。这个答案只会增加可能的混淆。 @AndréNeves 我发现这个答案很有帮助。 Object.create(...) 用于产生问题的 MDN 文章中,但未用于问题本身。我敢肯定很多人没有点击。 问题中引用的链接文章已经使用 Object.create()。这个答案和答案的编辑并不真正相关,至少可以说令人困惑:-) 更广泛的一点是,有一些陷阱会吸引刚接触 Javascript 原型的人。如果我们在 2016 年讨论,那么你真的应该使用 ES6 类、Babel 和/或 Typescript。但如果你真的想以这种方式手动构建类,它有助于理解原型链如何真正发挥作用以利用它们的力量。您可以使用任何对象作为原型,也许您不想新建一个单独的对象。此外,在 html 5 完全普及之前,Object.create 并不总是可用,因此更容易错误地设置类。【参考方案6】:

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

按照原来的例子,你有一个现有的对象student1

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

假设你不想知道student1是如何创建的,你只是想要另一个类似的对象,你可以使用student1的constructor属性like:

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

如果没有设置构造函数属性,这里将无法从Student 获取属性。相反,它将创建一个Person 对象。

【讨论】:

【参考方案7】:

有一个很好的代码示例说明为什么确实需要设置原型构造函数..

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"),而不是使用继承。 您的示例在内部使用this.constructor,因此必须设置它也就不足为奇了。你有没有它的例子吗?【参考方案8】:

现在不需要糖化功能“类”或使用“新”。使用对象字面量。

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

【讨论】:

【参考方案9】:

当您需要在没有猴子补丁的情况下替代 toString 时,这是必要的:

//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()??【参考方案10】:

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


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

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

【讨论】:

原型结构可能会变成一个人,但这是合适的,因为它继承了人的所有属性和方法。创建一个新的 Student() 而不设置原型。构造函数适当地调用它自己的构造函数。【参考方案11】:

给定简单的构造函数:

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 被第三方使用时,我们肯定应该这样做,而不是当代码最终在运行时执行时。

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

这是必要的。类继承中的任何类都必须有自己的构造函数,这样在原型继承中也是如此。也便于对象构造。但是这个问题是不必要的,需要理解的是JavaScript世界中调用函数作为构造函数的效果和解析对象属性的规则。

使用表达式new ( [ parameters] )

将函数作为构造函数执行的效果
    类型名称为函数名称的对象已创建 函数中的内部属性附加到创建的对象 函数的属性原型自动附加到创建的对象作为原型

对象属性解析规则

不仅会在对象上查找属性,还会在对象的原型、原型的原型等上查找属性,直到找到具有匹配名称的属性或到达原型链的末尾。

基于这些底层机制,语句 .prototype.constructor = 相当于在构造函数主体中附加构造函数并带有表达式this.constructor = 。如果是第二个话语,构造函数将在对象上解析,而如果是第一个话语,则在对象的原型上解析。

【讨论】:

【参考方案14】:

没必要。这只是传统的 OOP 拥护者尝试将 JavaScript 的原型继承转变为经典继承所做的众多事情之一。以下唯一的事情

Student.prototype.constructor = Student; 

确实,你现在有一个当前“构造函数”的引用。

在韦恩的回答中,这已被标记为正确,您可以与以下代码完全相同

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

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

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

感谢上帝,有了 ES6 经典继承,纯粹主义者可以使用语言的原生运算符,如 class、extends 和 super,而我们不必看到原型、构造函数更正和父引用。

【讨论】:

以上是关于为啥要设置原型构造函数?的主要内容,如果未能解决你的问题,请参考以下文章

构造函数和原型

js构造函数

同一个MFC工程中的两个普通类,为啥一个构造函数打点有反应,一个构造函数打点没反应?是因为设置问题吗

javascript 请记住在更改原型时设置构造函数属性

对象构造函数的原型图

“工厂构造原型” 设计模式