为啥要设置原型构造函数?
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 = subclass
,subclass.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.constructor
是Student
函数(因为您使用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
。所以这是误导。当然,如果您将使用它的东西(如Promise
或Array
)子类化,而不使用class
¹(它会为您处理),您需要确保正确设置它。所以基本上:这是个好主意。
如果您的代码(或您使用的库代码)中没有任何内容使用它,那也没关系。我一直确保它连接正确。
当然,使用 ES2015(又名 ES6)的 class
关键字,大多数时候我们会使用它,我们不必再使用它了,因为它会在我们使用时为我们处理
class Student extends Person
¹ “...如果您将使用它的东西(例如 Promise
或 Array
)子类化而不使用 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)
,这就是我们在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 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
-
类型名称为函数名称的对象已创建
函数中的内部属性附加到创建的对象
函数的属性原型自动附加到创建的对象作为原型
对象属性解析规则
不仅会在对象上查找属性,还会在对象的原型、原型的原型等上查找属性,直到找到具有匹配名称的属性或到达原型链的末尾。基于这些底层机制,语句
【讨论】:
【参考方案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,而我们不必看到原型、构造函数更正和父引用。
【讨论】:
以上是关于为啥要设置原型构造函数?的主要内容,如果未能解决你的问题,请参考以下文章