【问题标题】: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?我真的很想知道。

标签: javascript oop inheritance


【解决方案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。有关说明,请参阅stackoverflow.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] )

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

            对象属性解析规则

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

            基于这些底层机制,语句 .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,而我们不必看到原型、构造函数更正和父引用。

              【讨论】:

                猜你喜欢
                • 2012-03-09
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2012-03-05
                相关资源
                最近更新 更多